From 5ee2041769a3c6a7630ac663195edf30e555ff2d Mon Sep 17 00:00:00 2001 From: soraefir Date: Sat, 23 May 2026 15:09:28 +0200 Subject: [PATCH] init --- .github/workflows/build.yml | 68 + .gitignore | 16 + LICENSE | 674 ++++++++ PRIVACY.md | 12 + README.md | 107 ++ app/build.gradle | 111 ++ app/proguard-rules.pro | 17 + app/src/main/AndroidManifest.xml | 126 ++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 51639 bytes .../cowspent/android/about/AboutActivity.kt | 21 + .../cowspent/android/about/AboutScreen.kt | 153 ++ .../android/account/AccountActivity.kt | 339 ++++ .../cowspent/android/account/AccountScreen.kt | 266 +++ .../android/account/AccountViewModel.kt | 122 ++ .../cowspent/android/account/LoginDialog.kt | 117 ++ .../android/bill_edit/EditBillActivity.kt | 462 +++++ .../android/bill_edit/EditBillScreen.kt | 604 +++++++ .../android/bill_edit/EditBillViewModel.kt | 224 +++ .../android/bill_label/LabelBillsActivity.kt | 85 + .../android/bill_label/LabelBillsScreen.kt | 269 +++ .../android/bill_label/LabelBillsViewModel.kt | 101 ++ .../currencies/ManageCurrenciesActivity.kt | 145 ++ .../currencies/ManageCurrenciesScreen.kt | 178 ++ .../currencies/ManageCurrenciesViewModel.kt | 55 + .../helcel/cowspent/android/drawer/Drawer.kt | 350 ++++ .../cowspent/android/helper/AlertDialog.kt | 312 ++++ .../cowspent/android/helper/ColorPicker.kt | 298 ++++ .../android/helper/CommonComposables.kt | 85 + .../cowspent/android/helper/FormatUtils.kt | 20 + .../cowspent/android/helper/ModifierUtils.kt | 70 + .../android/helper/QrCodeScannerActivity.kt | 226 +++ .../cowspent/android/helper/TextDrawable.kt | 219 +++ .../helcel/cowspent/android/helper/Toaster.kt | 9 + .../cowspent/android/helper/UserAvatar.kt | 110 ++ .../android/main/BillsListComponents.kt | 163 ++ .../cowspent/android/main/BillsListScreen.kt | 586 +++++++ .../cowspent/android/main/BillsListUtils.kt | 117 ++ .../android/main/BillsListViewActivity.kt | 827 +++++++++ .../android/main/BillsListViewModel.kt | 77 + .../cowspent/android/main/MainConstants.kt | 36 + .../android/project/ProjectImportHelper.kt | 175 ++ .../android/project/ProjectOptionsDialog.kt | 156 ++ .../android/project/ProjectShareDialog.kt | 316 ++++ .../project/create/NewProjectActivity.kt | 347 ++++ .../project/create/NewProjectScreen.kt | 339 ++++ .../project/create/NewProjectViewModel.kt | 95 ++ .../project/edit/EditProjectActivity.kt | 192 +++ .../android/project/edit/EditProjectScreen.kt | 145 ++ .../project/edit/EditProjectViewModel.kt | 57 + .../android/project/member/MemberAddDialog.kt | 70 + .../project/member/MemberEditDialog.kt | 208 +++ .../project/member/MemberManagementDialog.kt | 132 ++ .../project/settle/ProjectSettlementScreen.kt | 401 +++++ .../android/settings/PreferencesActivity.kt | 71 + .../android/settings/SettingsScreen.kt | 449 +++++ .../statistics/ProjectSankeyDiagram.kt | 469 +++++ .../statistics/ProjectSpendingGraph.kt | 436 +++++ .../statistics/ProjectStatisticsActivity.kt | 56 + .../statistics/ProjectStatisticsPreview.kt | 48 + .../statistics/ProjectStatisticsScreen.kt | 155 ++ .../statistics/ProjectStatisticsTable.kt | 390 +++++ .../net/helcel/cowspent/model/Category.kt | 5 + .../net/helcel/cowspent/model/CreditDebt.kt | 3 + .../helcel/cowspent/model/DBAccountProject.kt | 17 + .../java/net/helcel/cowspent/model/DBBill.kt | 121 ++ .../net/helcel/cowspent/model/DBBillOwer.kt | 14 + .../net/helcel/cowspent/model/DBCategory.kt | 17 + .../net/helcel/cowspent/model/DBCurrency.kt | 17 + .../net/helcel/cowspent/model/DBMember.kt | 23 + .../helcel/cowspent/model/DBPaymentMode.kt | 17 + .../net/helcel/cowspent/model/DBProject.kt | 75 + .../net/helcel/cowspent/model/GroupedBill.kt | 26 + .../java/net/helcel/cowspent/model/Item.kt | 5 + .../net/helcel/cowspent/model/ProjectType.kt | 20 + .../net/helcel/cowspent/model/SectionItem.kt | 8 + .../net/helcel/cowspent/model/Transaction.kt | 3 + .../net/helcel/cowspent/model/UserItem.kt | 3 + .../model/parsed/AustrianBillQrCode.kt | 5 + .../model/parsed/CroatianBillQrCode.kt | 5 + .../persistence/CowspentSQLiteOpenHelper.kt | 1248 ++++++++++++++ .../persistence/CowspentServerSyncHelper.kt | 1501 +++++++++++++++++ .../net/helcel/cowspent/theme/ThemeUtils.kt | 119 ++ .../net/helcel/cowspent/util/BillFormatter.kt | 58 + .../net/helcel/cowspent/util/BillParser.kt | 68 + .../net/helcel/cowspent/util/CategoryUtils.kt | 37 + .../net/helcel/cowspent/util/ColorUtils.kt | 95 ++ .../helcel/cowspent/util/CospendClientUtil.kt | 121 ++ .../java/net/helcel/cowspent/util/Cowspent.kt | 31 + .../net/helcel/cowspent/util/ExportUtil.kt | 88 + .../net/helcel/cowspent/util/ICallback.kt | 10 + .../cowspent/util/IProjectCreationCallback.kt | 9 + .../util/IRefreshBillsListCallback.kt | 8 + .../helcel/cowspent/util/NextcloudClient.kt | 302 ++++ .../helcel/cowspent/util/ServerResponse.kt | 776 +++++++++ .../net/helcel/cowspent/util/SupportUtil.kt | 338 ++++ .../util/VersatileProjectSyncClient.kt | 1130 +++++++++++++ .../java/net/helcel/cowspent/util/evalMath.kt | 21 + .../res/drawable/ic_launcher_background.xml | 11 + .../res/drawable/ic_launcher_foreground.xml | 61 + app/src/main/res/drawable/test.xml | 52 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/resources.properties | 2 + app/src/main/res/values-night/booleans.xml | 4 + app/src/main/res/values-night/colors.xml | 11 + app/src/main/res/values-v27/styles.xml | 11 + app/src/main/res/values/booleans.xml | 4 + app/src/main/res/values/colors.xml | 14 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 282 ++++ app/src/main/res/values/styles.xml | 78 + .../main/res/xml/network_security_config.xml | 10 + app/src/main/res/xml/searchable.xml | 4 + build.gradle | 8 + gradle.properties | 28 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 251 +++ gradlew.bat | 94 ++ metadata/en-US/full_description.txt | 36 + metadata/en-US/images/icon.webp | Bin 0 -> 15418 bytes metadata/en-US/images/phoneScreenshots/1.png | Bin 0 -> 107203 bytes metadata/en-US/images/phoneScreenshots/2.png | Bin 0 -> 117522 bytes metadata/en-US/images/phoneScreenshots/3.png | Bin 0 -> 119001 bytes metadata/en-US/images/phoneScreenshots/4.png | Bin 0 -> 150412 bytes metadata/en-US/images/phoneScreenshots/5.png | Bin 0 -> 106318 bytes metadata/en-US/short_description.txt | 1 + metadata/en-US/title.txt | 1 + settings.gradle | 19 + 129 files changed, 19835 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 PRIVACY.md create mode 100644 README.md create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/net/helcel/cowspent/android/about/AboutActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/account/AccountViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/CommonComposables.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/ModifierUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/Toaster.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/helper/UserAvatar.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/BillsListViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/main/MainConstants.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsPreview.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt create mode 100644 app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/Category.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/CreditDebt.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBAccountProject.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBBill.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBBillOwer.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBCategory.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBCurrency.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBMember.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBPaymentMode.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/DBProject.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/GroupedBill.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/Item.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/ProjectType.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/SectionItem.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/Transaction.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/UserItem.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/parsed/AustrianBillQrCode.kt create mode 100644 app/src/main/java/net/helcel/cowspent/model/parsed/CroatianBillQrCode.kt create mode 100644 app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt create mode 100644 app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt create mode 100644 app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/BillFormatter.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/BillParser.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/ColorUtils.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/Cowspent.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/ExportUtil.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/ICallback.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/IProjectCreationCallback.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/IRefreshBillsListCallback.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/evalMath.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/test.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/resources.properties create mode 100644 app/src/main/res/values-night/booleans.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-v27/styles.xml create mode 100644 app/src/main/res/values/booleans.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/main/res/xml/searchable.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 metadata/en-US/full_description.txt create mode 100644 metadata/en-US/images/icon.webp create mode 100644 metadata/en-US/images/phoneScreenshots/1.png create mode 100644 metadata/en-US/images/phoneScreenshots/2.png create mode 100644 metadata/en-US/images/phoneScreenshots/3.png create mode 100644 metadata/en-US/images/phoneScreenshots/4.png create mode 100644 metadata/en-US/images/phoneScreenshots/5.png create mode 100644 metadata/en-US/short_description.txt create mode 100644 metadata/en-US/title.txt create mode 100644 settings.gradle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..82c5eaa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,68 @@ +#file: noinspection SpellCheckingInspection + +name: CI-Android APK + +env: + main_project_module: app + playstore_name: Beans + +on: + push: + branches: [ main ] + tags: + - '**' + pull_request: + branches: [ main ] + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: set up secrets + run: | + echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc + echo "${{ secrets.RELEASE_KEY}}" > key.asc + gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties + gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks + + - name: create and checkout branch + if: github.event_name == 'pull_request' + env: + BRANCH: ${{ github.head_ref }} + run: git checkout -B "$BRANCH" + + - name: set up JDK + uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: "temurin" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Build APK + run: | + VERSION_CODE=$(git rev-list --count HEAD) + ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE + + - name: Release + uses: softprops/action-gh-release@v3 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + app/build/outputs/apk/signedRelease/app-signedRelease.apk + prerelease: | + if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+\.[0-9]+[a-z]+$ ]]; then + echo "false" + else + echo "true" + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..509b477 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.gradle/ +local.properties +.DS_Store +build/ +captures/ +.idea/ +.kotlin/ +*.iml +.*.swp +projectFilesBackup/ +app/debug/ +app/lint/ +app/release/ +/gradle/verification-keyring.gpg +/crowdin.* +app/build \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..73dba86 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,12 @@ +# Cowspent Privacy Policy + +Cowspent does not collect any personal data. +This is a Free/Libre software so you can verify no data is collected +by reading the source code or asking someone to do it. + +Members and bills are synchronized with +the Cospend server of your choice. + +CAMERA permission is used to read QR-codes. + +GET\_ACCOUNTS permission is used by Single-Sign-On library to access accounts from Nextcloud Files app. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d92f56 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ + +
+

Cowspent

+ Logo + +

+Shared budget manager able to sync with [Nextcloud Cospend](https://github.com/julien-nc/cospend-nc). + +This work is based upon [MoneyBuster](https://gitlab.com/eneiluj/moneybuster). +Which is originally a fork of [PhoneTrack-Android](https://gitlab.com/eneiluj/phonetrack-android/). +Which is itself a fork of [Nextcloud Notes for Android](https://github.com/stefan-niedermann/nextcloud-notes). +Many thanks to their developers :heart: ! + + + +

+ +ko-fi +
+Built for Android +Built with love +
+ +Build Status + +
+ +## 🌄 Screenshots + +
+ + + + + + +
LauncherViewEdit
+
+ +## ⭐ Features + +- manage projects (add/remove/create/delete/edit) +- manage members (add/remove/edit) +- manage bills (add/remove/edit) +- search bills (by ~~payer~~, name, ~~amount~~, ~~date~~) - **still WIP** +- project statistics (Table, Graph and Sankey) with sharing +- project settlement plan with sharing +- dark theme and customizable main app color +- share/import projects with link/QRCode + +Extra Features: + +- Custom Split of bills +- Support for Cospend archived-status (hide archived projects by default) +- Modern UI and code with Kotlin and Jetpack-Compose + + +## 📳 Installation + + + +## ⚙️ Permissions + +- `CAMERA`: recommended for scanning barcodes from camera +- `INTERNET` and `ACCESS_NETWORK_STATE`: necessary for synchronization with Cospend/IHateMoney servers +- `GET_ACCOUNTS` necessary to log in with the Nextcloud account for Cospend + +## 📝 Contribute + +Cowspent is a user-driven project. We welcome any contribution, big or small. + +- **🖥️ Development:** Fix bugs, implement features, or research issues. Open a PR for review. +- **🍥 Design:** Improve interfaces, including accessibility and usability. +- **📂 Issue Reporting:** Report bugs and edge cases with relevant info. +- **🌍 Localization:** Translate if it doesn't support your language. + +## ✏️ Acknowledgements + +Thanks to all contributors, the developers of our dependencies, and our users. + +## 📝 License + +``` +Copyright (C) 2026 Helcel + +Cowspent Logo made with OpenMoji Asset under CC BY-SA 4.0 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..9a1a5aa --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,111 @@ +plugins { + id 'com.android.application' version '9.2.1' + id 'org.jetbrains.kotlin.plugin.serialization' version '2.3.21' + id 'org.jetbrains.kotlin.plugin.compose' version '2.3.21' +} + +android { + namespace = 'net.helcel.cowspent' + compileSdk = 37 + + defaultConfig { + buildConfigField("String", "APP_NAME", "\"Cowspent\"") + manifestPlaceholders["APP_NAME"] = "Cowspent" + applicationId "net.helcel.cowspent" + minSdk = 26 + targetSdk = 37 + versionName "1.0" + versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 + } + + signingConfigs { + register("release") { + try { + def keystorePropertiesFile = rootProject.file("app/keystore.properties") + def keystoreProperties = new Properties() + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } catch (FileNotFoundException e) { + println("File not found: ${e.message}") + } + } + } + + buildTypes { + debug { + debuggable true + initWith(buildTypes.release) + signingConfig = signingConfigs.debug + } + release { + minifyEnabled true + shrinkResources = true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + signedRelease { + minifyEnabled true + shrinkResources = true + initWith(buildTypes.release) + matchingFallbacks = ['release'] + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig = signingConfigs.getByName("release") + } + } + + compileOptions { + coreLibraryDesugaringEnabled = true + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + encoding = 'utf-8' + } + + buildFeatures { + viewBinding = true + buildConfig = true + compose = true + } + + kotlin { + jvmToolchain(21) + } + + lint { + abortOnError = false + disable 'MissingTranslation' + disable 'UsingMaterialAndMaterial3Libraries' + } + + androidResources { + generateLocaleConfig = true + } +} + +dependencies { + implementation 'androidx.compose.foundation:foundation:1.11.2' + implementation 'androidx.compose.runtime:runtime:1.11.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' + + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.work:work-runtime-ktx:2.11.2' + + implementation 'com.google.zxing:core:3.5.4' + implementation 'androidx.camera:camera-camera2:1.6.1' + implementation 'androidx.camera:camera-lifecycle:1.6.1' + implementation 'androidx.camera:camera-view:1.6.1' + implementation 'com.github.nextcloud:Android-SingleSignOn:1.1.0' + implementation 'com.opencsv:opencsv:5.12.0' + + implementation platform('androidx.compose:compose-bom:2026.05.01') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.material:material' + implementation 'androidx.compose.material:material-icons-extended' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + implementation 'androidx.activity:activity-compose:1.13.0' + implementation 'androidx.activity:activity-ktx:1.13.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..67f98d9 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\stnieder\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..67a0b67 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..212c605fb5d6c3b9512cc97d2d6d2b86827ae625 GIT binary patch literal 51639 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

>1u04ok=WZymle1xL&Akg~d+t zMjaQ^JkJ@!5y_^Cs}$M4S@AQBeog~j15*QA180LLYg6?~?Mb)xEoo*@|4r)&^PPUq zlWYPn!sx#xewA~9S_`Cn!W!}#>KeKlrkazVOmg#0^IzI&m_Z-O)ZVW^GjeuvO}?PM ztJPvBdJU>Ngpg+w%QQP<>-+Emz+kYgAY1*k% zHWJ@=m+aZB!`PlJ74>`nZFYl|(%WQjPsp{*bJ=xu+wt2obI)yiZhE0XzbGq&p@T(m zzUk++pPG{NyS%y{=EnDIwyG|u++QZ~Z#B=J9~na1_TRZ-cw?vR?UvlbW=YW-&)z7@ zao(nSJLLRF*CubE4>}4TJo@tGP1!u=>FIWMCqBzmu3Nil>y18-CBTYyE-{+WTu?|U1(SJ?Zn|$+s_xam&@tBot*t}|L6T*ZS2qgGXAsq z+yDE0`%h+8@x8MVlN0XmJ$@qjsp01vZ=WkGeQoDr5S+e&tMiP5;=%1tEk8T_DbIdz zj^~f$@Bivb8$DSYIyySIfo>2KEK~%h4<#i%V3KulInoM@g^rFL6M)(U1;v5+S4ruS j6ELm0xWsr+py{u;-p=iMo29OOVgLe9S3j3^P6 Unit +) { + val uriHandler = LocalUriHandler.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.simple_about)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(156.dp) + .clip(CircleShape) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Bold + ) + + Text( + text = stringResource(R.string.about_version, BuildConfig.VERSION_NAME), + style = MaterialTheme.typography.body2, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(48.dp)) + + AboutItem( + title = stringResource(R.string.about_maintainer_title), + content = stringResource(R.string.about_maintainer) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + AboutItem( + title = stringResource(R.string.about_license_title), + content = stringResource(R.string.about_license) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + val sourceUrl = stringResource(R.string.about_source) + AboutItem( + title = stringResource(R.string.about_source_title), + content = sourceUrl, + onClick = { uriHandler.openUri(sourceUrl) } + ) + } + } +} + +@Composable +fun AboutItem( + title: String, + content: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null +) { + var itemModifier = modifier.fillMaxWidth() + if (onClick != null) { + itemModifier = itemModifier.clickable(onClick = onClick) + } + + Column( + modifier = itemModifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title.uppercase(), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Bold + ) + Text( + text = content, + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + color = if (onClick != null) MaterialTheme.colors.primary else MaterialTheme.typography.body1.color, + textDecoration = if (onClick != null) TextDecoration.Underline else null + ) + } +} + +@Preview(showBackground = true) +@Composable +fun AboutScreenPreview() { + ThemeUtils.CowspentTheme { + AboutScreen(onBack = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt new file mode 100644 index 0000000..efe64de --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt @@ -0,0 +1,339 @@ +package net.helcel.cowspent.android.account + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.edit +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import com.nextcloud.android.sso.AccountImporter +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.nextcloud.android.sso.model.SingleSignOnAccount +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.main.MainConstants +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.CospendClientUtil +import net.helcel.cowspent.util.CospendClientUtil.LoginStatus +import java.net.URLDecoder +import java.util.Locale + + +class AccountActivity : AppCompatActivity() { + + private val viewModel: AccountViewModel by viewModels() + + companion object { + private val TAG = AccountActivity::class.java.simpleName + + const val SETTINGS_USE_SSO = "settingsUseSSO" + const val SETTINGS_SSO_URL = "settingsSSOUrl" + const val SETTINGS_SSO_USERNAME = "settingsSSOUsername" + const val SETTINGS_URL = "settingsUrl" + const val SETTINGS_USERNAME = "settingsUsername" + const val SETTINGS_PASSWORD = "settingsPassword" + const val SETTINGS_KEY_ETAG = "sessions_last_etag" + const val SETTINGS_KEY_LAST_MODIFIED = "sessions_last_modified" + const val DEFAULT_SETTINGS = "" + const val CREDENTIALS_CHANGED = 3 + + const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":" + const val WEBDAV_PATH_4_0_AND_LATER = "/remote.php/webdav" + } + + private lateinit var preferences: SharedPreferences + private var oldPassword = "" + private var useWebLogin = true + private var showLoginDialog by mutableStateOf(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModel.showWebView) { + viewModel.showWebView = false + } else { + finish() + } + } + }) + + setContent { + ThemeUtils.CowspentTheme { + if (viewModel.showWebView) { + val serverUrl = CospendClientUtil.formatURL(viewModel.serverUrl) + WebLoginScreen( + url = normalizeUrlSuffix(serverUrl) + "index.php/login/flow", + onLoginUrlDetected = { parseAndLoginFromWebView(it) } + ) + } else { + AccountScreen( + viewModel = viewModel, + onBack = { finish() }, + onConnect = { login() }, + onSsoClick = { isChecked -> + if (isChecked) { + showLoginDialog = true + } else { + viewModel.useSso = false + preferences.edit { putBoolean(SETTINGS_USE_SSO, false) } + } + }, + onLogout = { viewModel.logout() } + ) + } + + LoginDialog( + showDialog = showLoginDialog, + onDismissRequest = { showLoginDialog = false }, + onInitiateSsoLogin = { + showLoginDialog = false + try { + AccountImporter.pickNewAccount(this@AccountActivity) + } catch (e: Exception) { + Log.e(TAG, "Failed to initiate SSO login", e) + Toast.makeText(this@AccountActivity, "SSO login failed: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + ) + } + } + + oldPassword = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS) ?: "" + viewModel.validateUrl() + } + + @SuppressLint("SetJavaScriptEnabled") + @Composable + private fun WebLoginScreen(url: String, onLoginUrlDetected: (String) -> Unit) { + var isLoading by remember { mutableStateOf(true) } + + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { context -> + WebView(context).apply { + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + settings.apply { + allowFileAccess = false + javaScriptEnabled = true + domStorageEnabled = true + userAgentString = getWebLoginUserAgent(context) + } + webViewClient = object : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + if (url?.startsWith("nc://login/") == true) { + onLoginUrlDetected(url) + return true + } + return false + } + + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + val url = request?.url?.toString() + if (url?.startsWith("nc://login/") == true) { + onLoginUrlDetected(url) + return true + } + return false + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + isLoading = false + } + } + + val headers = HashMap() + headers["OCS-APIREQUEST"] = "true" + loadUrl(url, headers) + } + }, + modifier = Modifier.fillMaxSize() + ) + + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + } + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + try { + AccountImporter.onActivityResult(requestCode, resultCode, data, this + ) { ssoAccount -> + lifecycleScope.launch { + SingleAccountHelper.commitCurrentAccount(applicationContext, ssoAccount.name) + + preferences.edit { + putBoolean(SETTINGS_USE_SSO, true) + putString(SETTINGS_SSO_URL, ssoAccount.url) + putString(SETTINGS_SSO_USERNAME, ssoAccount.userId) + } + + viewModel.useSso = true + viewModel.serverUrl = ssoAccount.url + viewModel.username = ssoAccount.userId + + val resultData = Intent() + resultData.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED) + setResult(RESULT_OK, resultData) + finish() + } + } + } catch (e: Exception) { + Log.e(TAG, "SSO account selection failed", e) + } + } + + private fun legacyLogin() { + val url = CospendClientUtil.formatURL(viewModel.serverUrl.trim()) + val username = viewModel.username + var password = viewModel.password + + if (password.isEmpty()) { + password = oldPassword + } + + performLogin(url, username, password) + } + + private fun login() { + if (useWebLogin) { + viewModel.showWebView = true + } else { + legacyLogin() + } + } + + private fun getWebLoginUserAgent(context: Context): String { + val defaultUA = try { + android.webkit.WebSettings.getDefaultUserAgent(context) + } catch (_: Exception) { + Build.MANUFACTURER + " " + Build.MODEL + } + return "$defaultUA Cowspent/Android" + } + + private fun parseAndLoginFromWebView(dataString: String) { + try { + val loginUrlInfo = parseLoginDataUrl(dataString) + val url = normalizeUrlSuffix(loginUrlInfo.serverAddress) + performLogin(url, loginUrlInfo.username, loginUrlInfo.password) + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Invalid login URL", e) + } + } + + private fun parseLoginDataUrl(dataString: String): LoginUrlInfo { + val prefix = "nc://login/" + if (dataString.length < prefix.length) { + throw IllegalArgumentException("Invalid login URL detected") + } + val loginUrlInfo = LoginUrlInfo() + val data = dataString.substring(prefix.length) + val values = data.split("&") + + if (values.size !in 1..3) { + throw IllegalArgumentException("Illegal number of login URL elements detected: ${values.size}") + } + + for (value in values) { + when { + value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR") -> { + loginUrlInfo.username = URLDecoder.decode(value.substring(("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length), "UTF-8") + } + value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR") -> { + loginUrlInfo.password = URLDecoder.decode(value.substring(("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length), "UTF-8") + } + value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR") -> { + loginUrlInfo.serverAddress = URLDecoder.decode(value.substring(("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length), "UTF-8") + } + else -> throw IllegalArgumentException("Illegal magic login URL element detected: $value") + } + } + return loginUrlInfo + } + + private fun normalizeUrlSuffix(url: String): String { + return when { + url.lowercase(Locale.ROOT).endsWith(WEBDAV_PATH_4_0_AND_LATER) -> { + url.substring(0, url.length - WEBDAV_PATH_4_0_AND_LATER.length) + } + !url.endsWith("/") -> "$url/" + else -> url + } + } + + + private fun performLogin(url: String, username: String, password: String) { + viewModel.isSubmitting = true + viewModel.showWebView = false + + lifecycleScope.launch { + val status = withContext(Dispatchers.IO) { + CospendClientUtil.isValidLogin(url, username, password) + } + + if (status == LoginStatus.OK) { + preferences.edit { + putString(SETTINGS_URL, url) + putString(SETTINGS_USERNAME, username) + putString(SETTINGS_PASSWORD, password) + remove(SETTINGS_KEY_ETAG) + remove(SETTINGS_KEY_LAST_MODIFIED) + } + + val data = Intent() + data.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED) + setResult(RESULT_OK, data) + finish() + } else { + Log.e("Cowspent", "invalid login") + viewModel.isSubmitting = false + Toast.makeText(applicationContext, getString(R.string.error_invalid_login, getString(status.str)), Toast.LENGTH_LONG).show() + } + } + } + + + class LoginUrlInfo { + var serverAddress: String = "" + var username: String = "" + var password: String = "" + } + +} diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt new file mode 100644 index 0000000..9c7c213 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt @@ -0,0 +1,266 @@ +@file:Suppress("SameParameterValue", "SameParameterValue", "SameParameterValue", + "SameParameterValue", "SameParameterValue", "SameParameterValue" +) + +package net.helcel.cowspent.android.account + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import net.helcel.cowspent.R +import net.helcel.cowspent.theme.ThemeUtils + +@Composable +fun AccountScreen( + viewModel: AccountViewModel, + onBack: () -> Unit, + onConnect: () -> Unit, + onSsoClick: (Boolean) -> Unit, + onLogout: () -> Unit +) { + AccountScreenContent( + isLoggedIn = viewModel.isLoggedIn, + isValidatingLogin = viewModel.isValidatingLogin, + useSso = viewModel.useSso, + serverUrl = viewModel.serverUrl, + username = viewModel.username, + password = viewModel.password, + isUrlValid = viewModel.isUrlValid, + showUrlWarning = viewModel.showUrlWarning, + isSubmitting = viewModel.isSubmitting, + isFormValid = viewModel.isFormValid, + onServerUrlChange = { + viewModel.serverUrl = it + viewModel.validateUrl() + }, + onUsernameChange = { viewModel.username = it }, + onPasswordChange = { viewModel.password = it }, + onBack = onBack, + onConnect = onConnect, + onSsoClick = onSsoClick, + onLogout = onLogout + ) +} + +@Composable +fun AccountScreenContent( + isLoggedIn: Boolean, + isValidatingLogin: Boolean, + useSso: Boolean, + serverUrl: String, + username: String, + password: String, + isUrlValid: Boolean, + showUrlWarning: Boolean, + isSubmitting: Boolean, + isFormValid: Boolean, + onServerUrlChange: (String) -> Unit, + onUsernameChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onBack: () -> Unit, + onConnect: () -> Unit, + onSsoClick: (Boolean) -> Unit, + onLogout: () -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.settings_server_settings)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + .fillMaxSize() + ) { + if (isValidatingLogin) { + Box(modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else if (isLoggedIn) { + Text( + text = stringResource(R.string.account_logged_in_as, username), + style = MaterialTheme.typography.h6 + ) + Text( + text = serverUrl, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = onLogout, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.account_logout)) + } + Spacer(modifier = Modifier.height(24.dp)) + } + + if (!isValidatingLogin) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 8.dp) + ) { + Text(stringResource(R.string.use_sso_toggle), modifier = Modifier.weight(1f)) + Switch( + checked = useSso, + onCheckedChange = { onSsoClick(it) }, + colors = SwitchDefaults.colors( + uncheckedThumbColor = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + uncheckedTrackColor = MaterialTheme.colors.onSurface.copy(alpha = 0.3f) + ) + ) + } + + if (!useSso) { + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = serverUrl, + onValueChange = onServerUrlChange, + label = { Text(stringResource(R.string.settings_url)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) }, + trailingIcon = { + if (isUrlValid) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = Color.Green + ) + } + }, + singleLine = true + ) + + if (showUrlWarning) { + Text( + stringResource(R.string.settings_url_warn_http), + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = username, + onValueChange = onUsernameChange, + label = { Text(stringResource(R.string.settings_username)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) }, + singleLine = true + ) + + Spacer(modifier = Modifier.height(8.dp)) + + @Suppress("DEPRECATION") + OutlinedTextField( + value = password, + onValueChange = onPasswordChange, + label = { Text(stringResource(R.string.settings_password)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = onConnect, + modifier = Modifier.fillMaxWidth(), + enabled = isFormValid && !isSubmitting + ) { + if (isSubmitting) { + CustomCircularProgressIndicator(size = 24.dp, color = Color.White) + } else { + Text(stringResource(R.string.settings_submit)) + } + } + } + } + } + } +} + +@Composable +fun CustomCircularProgressIndicator(size: Dp, color: Color) { + CircularProgressIndicator( + modifier = Modifier.size(size), + color = color, + strokeWidth = 2.dp + ) +} + +@Preview(showBackground = true) +@Composable +fun AccountScreenPreview() { + ThemeUtils.CowspentTheme { + AccountScreenContent( + isLoggedIn = false, + isValidatingLogin = false, + useSso = false, + serverUrl = "https://nextcloud.example.com", + username = "user", + password = "", + isUrlValid = true, + showUrlWarning = false, + isSubmitting = false, + isFormValid = true, + onServerUrlChange = {}, + onUsernameChange = {}, + onPasswordChange = {}, + onBack = {}, + onConnect = {}, + onSsoClick = {}, + onLogout = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountViewModel.kt new file mode 100644 index 0000000..8966d0c --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountViewModel.kt @@ -0,0 +1,122 @@ +package net.helcel.cowspent.android.account + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.preference.PreferenceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.util.CospendClientUtil + +class AccountViewModel(application: Application) : AndroidViewModel(application) { + private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) + + var useSso by mutableStateOf(preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) + var serverUrl by mutableStateOf( + if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + preferences.getString(AccountActivity.SETTINGS_SSO_URL, "") ?: "" + } else { + preferences.getString(AccountActivity.SETTINGS_URL, "") ?: "" + } + ) + var username by mutableStateOf( + if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + preferences.getString(AccountActivity.SETTINGS_SSO_USERNAME, "") ?: "" + } else { + preferences.getString(AccountActivity.SETTINGS_USERNAME, "") ?: "" + } + ) + var password by mutableStateOf("") + + var isUrlValid by mutableStateOf(false) + var isSubmitting by mutableStateOf(false) + var showUrlWarning by mutableStateOf(false) + var showWebView by mutableStateOf(false) + + var isLoggedIn by mutableStateOf(false) + private set + + var isValidatingLogin by mutableStateOf(false) + private set + + init { + checkLoginStatus() + } + + private fun checkLoginStatus() { + val url = if (useSso) { + preferences.getString(AccountActivity.SETTINGS_SSO_URL, "") + } else { + preferences.getString(AccountActivity.SETTINGS_URL, "") + } + val username = if (useSso) { + preferences.getString(AccountActivity.SETTINGS_SSO_USERNAME, "") + } else { + preferences.getString(AccountActivity.SETTINGS_USERNAME, "") + } + val password = if (useSso) { + "" + } else { + preferences.getString(AccountActivity.SETTINGS_PASSWORD, "") + } + + if (!url.isNullOrEmpty() && !username.isNullOrEmpty()) { + viewModelScope.launch { + isValidatingLogin = true + isLoggedIn = withContext(Dispatchers.IO) { + if (useSso) { + true + } else { + !password.isNullOrEmpty() && + CospendClientUtil.isValidLogin( + url, + username, + password + ) == CospendClientUtil.LoginStatus.OK + } + } + isValidatingLogin = false + } + } + } + + fun logout() { + preferences.edit { + remove(AccountActivity.SETTINGS_USE_SSO) + remove(AccountActivity.SETTINGS_SSO_URL) + remove(AccountActivity.SETTINGS_SSO_USERNAME) + remove(AccountActivity.SETTINGS_URL) + remove(AccountActivity.SETTINGS_USERNAME) + remove(AccountActivity.SETTINGS_PASSWORD) + remove(AccountActivity.SETTINGS_KEY_ETAG) + remove(AccountActivity.SETTINGS_KEY_LAST_MODIFIED) + } + + useSso = false + serverUrl = "" + username = "" + password = "" + isLoggedIn = false + } + + fun validateUrl() { + val formattedUrl = CospendClientUtil.formatURL(serverUrl) + showUrlWarning = CospendClientUtil.isHttp(formattedUrl) && !useSso + + viewModelScope.launch { + val valid = withContext(Dispatchers.IO) { + CospendClientUtil.isValidURL(formattedUrl) + } + isUrlValid = valid + } + } + + val isFormValid: Boolean + get() = useSso || (isUrlValid && username.isNotEmpty()) +} diff --git a/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt b/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt new file mode 100644 index 0000000..c630148 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt @@ -0,0 +1,117 @@ +package net.helcel.cowspent.android.account + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun LoginDialog( + showDialog: Boolean, + onDismissRequest: () -> Unit, + onInitiateSsoLogin: () -> Unit, + errorMessage: String? = null +) { + if (showDialog) { + AlertDialog( + onDismissRequest = onDismissRequest, + shape = MaterialTheme.shapes.large, + title = { Text("Login") }, + text = { + if (errorMessage != null) { + Text(text = errorMessage) + } else { + Text(text = "Please choose a login method.") + } + }, + confirmButton = { + TextButton(onClick = onInitiateSsoLogin) { + Text("Login with Nextcloud SSO") + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text("Cancel") + } + }, + modifier = Modifier + ) + } +} + +@Composable +fun LoginDialogContent( + onInitiateSsoLogin: () -> Unit, + onDismissRequest: () -> Unit, + errorMessage: String? = null +) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text( + text = "Login", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (errorMessage != null) { + Text(text = errorMessage) + } else { + Text(text = "Please choose a login method.") + } + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismissRequest) { + Text("Cancel") + } + TextButton(onClick = onInitiateSsoLogin) { + Text("Login with Nextcloud SSO") + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun LoginDialogPreview() { + MaterialTheme { + LoginDialogContent( + onDismissRequest = {}, + onInitiateSsoLogin = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun LoginDialogErrorPreview() { + MaterialTheme { + LoginDialogContent( + onDismissRequest = {}, + onInitiateSsoLogin = {}, + errorMessage = "Invalid credentials. Please try again." + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt new file mode 100644 index 0000000..4681bfc --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -0,0 +1,462 @@ +package net.helcel.cowspent.android.bill_edit + +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.remember +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.QrCodeScannerActivity +import net.helcel.cowspent.android.helper.showToast +import net.helcel.cowspent.android.main.MainConstants +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBBillOwer +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.BillParser +import net.helcel.cowspent.util.CategoryUtils +import java.text.ParseException +import java.time.ZoneId +import java.util.Calendar + +class EditBillActivity : AppCompatActivity() { + + private val viewModel: EditBillViewModel by viewModels() + private lateinit var db: CowspentSQLiteOpenHelper + private lateinit var bill: DBBill + private var projectType: ProjectType = ProjectType.LOCAL + private val calendar = Calendar.getInstance() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + db = CowspentSQLiteOpenHelper.getInstance(this) + + lifecycleScope.launch { + initBill() + + setContent { + ThemeUtils.CowspentTheme { + + val categories = remember { + val syncedCategories = db.getCategories(bill.projectId) + val defaultCategories = CategoryUtils.getDefaultCategories(this@EditBillActivity, bill.projectId) + val hardcoded = if (projectType == ProjectType.LOCAL) { + defaultCategories + } else { + listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT }) + } + syncedCategories + hardcoded + } + val paymentModes = remember { + val syncedPaymentModes = db.getPaymentModes(bill.projectId) + val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId) + if (projectType == ProjectType.LOCAL) { + syncedPaymentModes + defaultPaymentModes + } else { + syncedPaymentModes.ifEmpty { defaultPaymentModes } + } + } + + EditBillScreen( + viewModel = viewModel, + categories = categories, + paymentModes = paymentModes, + onSave = { saveBillAsked() }, + onBack = { onBack() }, + onDateClick = { + DatePickerDialog( + this@EditBillActivity, + { _, year, month, day -> + calendar[Calendar.YEAR] = year + calendar[Calendar.MONTH] = month + calendar[Calendar.DAY_OF_MONTH] = day + viewModel.timestamp = calendar.timeInMillis / 1000 + }, + calendar[Calendar.YEAR], + calendar[Calendar.MONTH], + calendar[Calendar.DAY_OF_MONTH] + ).show() + }, + onTimeClick = { + TimePickerDialog( + this@EditBillActivity, + { _, hour, minute -> + calendar[Calendar.HOUR_OF_DAY] = hour + calendar[Calendar.MINUTE] = minute + viewModel.timestamp = calendar.timeInMillis / 1000 + }, + calendar[Calendar.HOUR_OF_DAY], + calendar[Calendar.MINUTE], + true + ).show() + }, + onScan = { + val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java) + scanQRCodeLauncher.launch(createIntent) + }, + onDelete = if (bill.id > 0) { { deleteBillAsked() } } else null, + accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN + ) + } + } + } + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onBack() + } + }) + } + + private suspend fun initBill() { + val billId = intent.getLongExtra(PARAM_BILL_ID, 0) + val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS) + val projectId = intent.getLongExtra(PARAM_PROJECT_ID, 0) + val projectTypeStr = intent.getStringExtra(PARAM_PROJECT_TYPE) + projectType = if (!projectTypeStr.isNullOrEmpty()) { + ProjectType.getTypeById(projectTypeStr) ?: ProjectType.LOCAL + } else { + ProjectType.LOCAL + } + + withContext(Dispatchers.IO) { + var customSplits: Map? = null + if (groupedBillIds != null && groupedBillIds.isNotEmpty()) { + val sourceBills: List = groupedBillIds.map { db.getBill(it) }.filterNotNull() + if (sourceBills.isNotEmpty()) { + val first = sourceBills[0] + val totalAmount = sourceBills.sumOf { it.amount } + bill = DBBill( + first.id, 0, first.projectId, first.payerId, totalAmount, + first.timestamp, first.what, first.state, first.repeat, + first.paymentMode, first.categoryRemoteId, first.comment, first.paymentModeRemoteId + ) + + val splits = mutableMapOf() + for (b in sourceBills) { + val owersCount = b.billOwers.size + if (owersCount > 0) { + val part = b.amount / owersCount + for (ower in b.billOwers) { + splits[ower.memberId] = (splits[ower.memberId] ?: 0.0) + part + } + } + } + customSplits = splits + } + } else if (billId > 0) { + bill = db.getBill(billId)!! + } else { + val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0) + val timeNowSeconds = System.currentTimeMillis() / 1000 + if (billIdToDuplicate == 0L) { + bill = DBBill( + 0, 0, projectId, 0, 0.0, timeNowSeconds, + "", DBBill.STATE_ADDED, DBBill.NON_REPEATED, + DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE + ) + } else { + val btd = db.getBill(billIdToDuplicate)!! + bill = DBBill( + 0, 0, projectId, btd.payerId, btd.amount, + timeNowSeconds, btd.what, DBBill.STATE_ADDED, + btd.repeat, btd.paymentMode, btd.categoryRemoteId, + btd.comment, btd.paymentModeRemoteId + ) + val btdOwers = btd.billOwers + val newBillOwers = btdOwers.filter { + val m = db.getMember(it.memberId) + m != null && m.isActivated + } + bill.billOwers = newBillOwers + } + } + calendar.timeInMillis = bill.timestamp * 1000 + val members = db.getMembersOfProject(bill.projectId, null) + val project = db.getProject(bill.projectId) + val currencies = db.getCurrencies(bill.projectId) + withContext(Dispatchers.Main) { + viewModel.currencies = currencies + viewModel.mainCurrencyName = project?.currencyName ?: "" + viewModel.initFromBill(bill, members, customSplits) + } + } + } + + private val scanQRCodeLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK && result.data != null) { + val scannedBill = result.data?.getStringExtra(MainConstants.KEY_QR_CODE) + if (scannedBill != null) { + try { + val austrianBill = BillParser.parseAustrianBillFromQrCode(scannedBill) + calendar.timeInMillis = austrianBill.date.time + viewModel.timestamp = calendar.timeInMillis / 1000 + viewModel.amount = austrianBill.amount.toString() + return@registerForActivityResult + } catch (_: ParseException) { + } + try { + val croatianBill = BillParser.parseCroatianBillFromQrCode(scannedBill) + if (croatianBill.date != null) { + calendar.timeInMillis = + croatianBill.date.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + viewModel.timestamp = calendar.timeInMillis / 1000 + } + viewModel.amount = croatianBill.amount.toString() + return@registerForActivityResult + } catch (_: ParseException) { + } + showToast(this, getString(R.string.error_scanning_bill_qr_code)) + } + } + } + + private fun onBack() { + if (!valuesHaveChanged()) { + finish() + return + } + viewModel.showDialog( + title = getString(R.string.save_or_discard_bill_dialog_title), + message = getString(R.string.save_or_discard_bill_dialog_message), + positiveText = getString(R.string.save_or_discard_bill_dialog_save), + onConfirm = { saveBillAsked() }, + negativeText = getString(R.string.save_or_discard_bill_dialog_discard), + onCancel = { finish() } + ) + } + + private fun saveBillAsked() { + val validationError = viewModel.getValidationError( + getString(R.string.error_invalid_bill_what), + getString(R.string.error_invalid_bill_date), + getString(R.string.error_invalid_bill_payerid), + getString(R.string.error_invalid_bill_owers), + getString(R.string.simple_error) + ) + + if (validationError != null) { + showToast(this, validationError) + } else { + lifecycleScope.launch { + val savedBillId = withContext(Dispatchers.IO) { saveBill() } + val data = Intent() + data.putExtra(MainConstants.SAVED_BILL_ID, savedBillId) + setResult(RESULT_OK, data) + finish() + } + } + } + + private fun deleteBillAsked() { + viewModel.showDialog( + title = getString(R.string.confirm_remove_project_dialog_title), + message = bill.what, + positiveText = getString(R.string.action_delete), + onConfirm = { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS) + if (groupedBillIds != null && groupedBillIds.isNotEmpty()) { + for (id in groupedBillIds) { + db.setBillState(id, DBBill.STATE_DELETED) + } + } else if (bill.id > 0) { + db.setBillState(bill.id, DBBill.STATE_DELETED) + } + val proj = db.getProject(bill.projectId) + if (proj != null) db.syncIfRemote(proj) + } + val data = Intent() + data.putExtra(MainConstants.DELETED_BILL, bill.id) + setResult(RESULT_OK, data) + finish() + } + }, + negativeText = getString(R.string.simple_cancel) + ) + } + + private fun valuesHaveChanged(): Boolean { + val newOwersIds = viewModel.getOwersIds().toSet() + val billOwersIds = bill.billOwersIds.toSet() + val owersChanged = newOwersIds != billOwersIds + + return !(bill.what == viewModel.what && + bill.timestamp == viewModel.timestamp && + bill.amount == viewModel.amountAsDouble && + bill.payerId == viewModel.payerId && + bill.comment == viewModel.comment && + bill.repeat == viewModel.repeat && + bill.categoryRemoteId == viewModel.categoryRemoteId && + bill.paymentModeRemoteId == viewModel.paymentModeRemoteId && + !owersChanged) + } + + private suspend fun saveBill(): Long = withContext(Dispatchers.IO) { + val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS) + val isCustomSplit = viewModel.isCustomSplit + + if (isCustomSplit) { + val splits = viewModel.owersCustomSplit.filter { (id, amount) -> + viewModel.owersSelection[id] == true && (amount.replace(',', '.').toDoubleOrNull() + ?: 0.0) > 0 + }.mapValues { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 } + + if (splits.isEmpty()) return@withContext 0L + + val splitEntries = splits.entries.toList() + + // Pool of existing bills in this group that we can potentially reuse + val billsToPool = mutableListOf() + if (bill.id != 0L) billsToPool.add(bill.id) + groupedBillIds?.forEach { if (it != bill.id) billsToPool.add(it) } + + val processedBillIds = mutableSetOf() + var firstSavedId = 0L + + for (entry in splitEntries) { + val memberId = entry.key + val amount = entry.value + + // Try to find a bill in the pool that already exists for this exact member + var billToUseId = billsToPool.find { id -> + val b = db.getBill(id) + b?.billOwers?.size == 1 && b.billOwers[0].memberId == memberId + } + + // Fallback: just take any available bill from the pool + if (billToUseId == null) { + billToUseId = billsToPool.firstOrNull() + } + + if (billToUseId != null) { + billsToPool.remove(billToUseId) + processedBillIds.add(billToUseId) + + val existingBill = db.getBill(billToUseId)!! + db.updateBillAndSync( + existingBill, + viewModel.payerId, + amount, + viewModel.timestamp, + viewModel.what, + listOf(memberId), + viewModel.repeat, + existingBill.paymentMode, + viewModel.paymentModeRemoteId, + viewModel.categoryRemoteId, + viewModel.comment + ) + if (firstSavedId == 0L) firstSavedId = billToUseId + } else { + // Create a new bill for this payee + val newBill = DBBill( + 0, 0, bill.projectId, viewModel.payerId, amount, + viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat, + bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId + ) + newBill.billOwers = listOf(DBBillOwer(0, 0, memberId)) + val newId = db.addBill(newBill) + if (firstSavedId == 0L) firstSavedId = newId + } + } + + // Mark any remaining bills in the original group as deleted + for (id in billsToPool) { + db.setBillState(id, DBBill.STATE_DELETED) + } + + val proj = db.getProject(bill.projectId) + if (proj != null) db.syncIfRemote(proj) + db.updateProject( + bill.projectId, + null, + null, + null, + viewModel.payerId, + null, + null, + null, + null, + null + ) + + return@withContext firstSavedId + } else { + val newAmount = viewModel.amountAsDouble + val newOwersIds = viewModel.getOwersIds() + + if (bill.id != 0L) { + if (valuesHaveChanged()) { + db.updateBillAndSync( + bill, + viewModel.payerId, + newAmount, + viewModel.timestamp, + viewModel.what, + newOwersIds, + viewModel.repeat, + bill.paymentMode, + viewModel.paymentModeRemoteId, + viewModel.categoryRemoteId, + viewModel.comment + ) + if (groupedBillIds != null) { + for (id in groupedBillIds) { + if (id != bill.id) { + db.setBillState(id, DBBill.STATE_DELETED) + } + } + val proj = db.getProject(bill.projectId) + if (proj != null) db.syncIfRemote(proj) + } + } + return@withContext bill.id + } else { + val newBill = DBBill( + 0, 0, bill.projectId, viewModel.payerId, newAmount, + viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat, + bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId + ) + newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) } + val newBillId = db.addBill(newBill) + db.updateProject( + bill.projectId, + null, + null, + null, + viewModel.payerId, + null, + null, + null, + null, + null + ) + val proj = db.getProject(bill.projectId) + if (proj != null) db.syncIfRemote(proj) + return@withContext newBillId + } + } + } + + companion object { + const val PARAM_BILL_ID = "billId" + const val PARAM_GROUPED_BILL_IDS = "grouped_bill_ids" + const val PARAM_PROJECT_ID = "projectId" + const val PARAM_PROJECT_TYPE = "projectType" + const val PARAM_BILL_ID_TO_DUPLICATE = "billToDuplicate" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt new file mode 100644 index 0000000..242356c --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt @@ -0,0 +1,604 @@ +package net.helcel.cowspent.android.bill_edit + +import android.annotation.SuppressLint +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Comment +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.* +import net.helcel.cowspent.model.* +import net.helcel.cowspent.util.SupportUtil +import java.util.Date +import kotlin.math.abs + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun EditBillScreen( + viewModel: EditBillViewModel, + categories: List, + paymentModes: List, + onSave: () -> Unit, + onBack: () -> Unit, + onDateClick: () -> Unit, + onTimeClick: () -> Unit, + onScan: () -> Unit, + onDelete: (() -> Unit)? = null, + accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN +) { + val canEdit = accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN || accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT + val context = LocalContext.current + + StatefulAlertDialog( + state = viewModel.dialogState, + onDismissRequest = { viewModel.dismissDialog() } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(if (viewModel.what.isEmpty()) R.string.simple_new_bill else R.string.simple_edit_bill)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + if (canEdit) { + IconButton(onClick = onScan) { + Icon(Icons.Default.QrCodeScanner, contentDescription = null) + } + if (onDelete != null) { + IconButton(onClick = onDelete) { + Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete)) + } + } + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + }, + floatingActionButton = { + if (canEdit) { + val errorWhat = stringResource(R.string.error_invalid_bill_what) + val errorDate = stringResource(R.string.error_invalid_bill_date) + val errorPayer = stringResource(R.string.error_invalid_bill_payerid) + val errorOwers = stringResource(R.string.error_invalid_bill_owers) + val errorInvalidForm = stringResource(R.string.simple_error) + + FloatingActionButton(onClick = { + val validationError = viewModel.getValidationError( + errorWhat, errorDate, errorPayer, errorOwers, errorInvalidForm + ) + if (validationError == null) { + onSave() + } else { + showToast(context, validationError) + } + }) { + Icon( + Icons.Default.Done, + contentDescription = stringResource(R.string.action_save_bill) + ) + } + } + } + ) { padding -> + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .padding(padding) + .padding(16.dp) + .fillMaxSize() + .verticalScroll(scrollState) + ) { + BillBasicInfoSection( + viewModel = viewModel, + canEdit = canEdit, + onDateClick = onDateClick, + onTimeClick = onTimeClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PayerSection( + viewModel = viewModel, + canEdit = canEdit + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OwerSelectionSection( + viewModel = viewModel, + canEdit = canEdit + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BillAdditionalDetailsSection( + viewModel = viewModel, + categories = categories, + paymentModes = paymentModes, + canEdit = canEdit + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +fun BillBasicInfoSection( + viewModel: EditBillViewModel, + canEdit: Boolean, + onDateClick: () -> Unit, + onTimeClick: () -> Unit +) { + val context = LocalContext.current + val currencyDialogTitle = + stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName) + val noCurrencyError = stringResource(R.string.no_currency_error) + + OutlinedTextField( + value = viewModel.what, + onValueChange = { viewModel.what = it }, + enabled = canEdit, + placeholder = { Text(stringResource(R.string.setting_what)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = viewModel.amount, + onValueChange = { + viewModel.amount = it + viewModel.updateSplits() + }, + enabled = canEdit, + placeholder = { Text("0") }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.AttachMoney, contentDescription = null) }, + trailingIcon = { + IconButton( + enabled = canEdit, + onClick = { + if (viewModel.currencies.isNotEmpty()) { + viewModel.showDialog( + title = currencyDialogTitle, + items = viewModel.currencies.map { "${it.name} (${it.exchangeRate})" }, + onItemSelected = { index -> + viewModel.convertCurrency(viewModel.currencies[index]) + } + ) + } else { + showToast(context, noCurrencyError) + } + } + ) { + Icon(Icons.Default.CurrencyExchange, contentDescription = null) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val dateFormat = + remember(context) { android.text.format.DateFormat.getDateFormat(context) } + val timeFormat = + remember(context) { android.text.format.DateFormat.getTimeFormat(context) } + val dateStr = dateFormat.format(Date(viewModel.timestamp * 1000)) + val timeStr = timeFormat.format(Date(viewModel.timestamp * 1000)) + + Row { + ClickableOutlinedTextField( + value = dateStr, + onClick = onDateClick, + modifier = Modifier.weight(1f), + enabled = canEdit, + leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) } + ) + Spacer(modifier = Modifier.width(8.dp)) + ClickableOutlinedTextField( + value = timeStr, + onClick = onTimeClick, + modifier = Modifier.weight(1f), + enabled = canEdit, + leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = null) } + ) + } +} + +@Composable +fun PayerSection( + viewModel: EditBillViewModel, + canEdit: Boolean +) { + var payerExpanded by remember { mutableStateOf(false) } + val selectedPayer = viewModel.members.find { it.id == viewModel.payerId } + + EditableExposedDropdownMenu( + value = selectedPayer?.name ?: "", + placeholder = stringResource(R.string.setting_payer), + expanded = payerExpanded, + onExpandedChange = { payerExpanded = it }, + onDismissRequest = { payerExpanded = false }, + enabled = canEdit, + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedPayer != null) { + UserAvatar( + name = selectedPayer.name, + r = selectedPayer.r, + g = selectedPayer.g, + b = selectedPayer.b, + disabled = !selectedPayer.isActivated, + size = 24.dp + ) + } else { + Icon(Icons.Default.Person, contentDescription = null) + } + } + }, + content = { + viewModel.members.forEach { member -> + DropdownMenuItem(onClick = { + viewModel.payerId = member.id + payerExpanded = false + }) { + UserAvatar( + name = member.name, + r = member.r, + g = member.g, + b = member.b, + disabled = !member.isActivated, + size = 24.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(member.name) + } + } + } + ) +} + +@Composable +fun OwerSelectionSection( + viewModel: EditBillViewModel, + canEdit: Boolean +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.width(8.dp)) + if (viewModel.owersSelection.all { it.value }) { + IconButton( + enabled = canEdit, + onClick = { + viewModel.members.forEach { viewModel.owersSelection[it.id] = false } + viewModel.updateSplits() + }, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.RemoveDone, + contentDescription = stringResource(R.string.setting_none), + modifier = Modifier.scale(0.8f) + ) + } + } else { + IconButton( + enabled = canEdit, + onClick = { + viewModel.members.forEach { viewModel.owersSelection[it.id] = true } + viewModel.updateSplits() + }, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.DoneAll, + contentDescription = stringResource(R.string.setting_all), + modifier = Modifier.scale(0.8f) + ) + } + } + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(R.string.setting_owers), fontSize = 12.sp, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + Spacer(Modifier.weight(1f)) + + if (viewModel.isCustomSplit) { + val diff = viewModel.getDiffSplit() + if (abs(diff) > 0.01) { + val diffText = + if (diff > 0) "Missing: ${SupportUtil.normalNumberFormat.format(diff)}" else "Excess: ${ + SupportUtil.normalNumberFormat.format(-diff) + }" + Text( + diffText, + color = MaterialTheme.colors.error, + fontSize = 12.sp, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } else { + Text("Even Split", fontSize = 12.sp) + } + Switch( + checked = !viewModel.isCustomSplit, + enabled = canEdit, + onCheckedChange = { + viewModel.isCustomSplit = !it + viewModel.updateSplits() + }, + colors = SwitchDefaults.colors( + uncheckedThumbColor = MaterialTheme.colors.onSurface, + ) + ) + } + + viewModel.members.forEach { member -> + val isSelected = viewModel.owersSelection[member.id] ?: false + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = canEdit) { + viewModel.toggleMember(member.id, !isSelected) + } + + ) { + Checkbox( + checked = isSelected, + enabled = canEdit, + onCheckedChange = { viewModel.toggleMember(member.id, it) } + ) + UserAvatar( + name = member.name, + r = member.r, + g = member.g, + b = member.b, + disabled = !member.isActivated, + size = 32.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(member.name, modifier = Modifier.weight(1f)) + + if (isSelected || viewModel.isCustomSplit) { + val interactionSource = remember { MutableInteractionSource() } + BasicTextField( + value = viewModel.owersCustomSplit[member.id] ?: "", + onValueChange = { + viewModel.owersCustomSplit[member.id] = it + viewModel.owersSelection[member.id] = (it != "") + }, + modifier = Modifier + .width(80.dp) + .height(46.dp), + interactionSource = interactionSource, + singleLine = true, + enabled = viewModel.isCustomSplit && canEdit, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + cursorBrush = SolidColor(MaterialTheme.colors.onSurface), + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Right, + color = MaterialTheme.colors.onSurface + ), + ) { innerTextField -> + TextFieldDefaults.OutlinedTextFieldDecorationBox( + value = viewModel.owersCustomSplit[member.id] ?: "", + visualTransformation = VisualTransformation.None, + innerTextField = innerTextField, + singleLine = true, + enabled = viewModel.isCustomSplit && canEdit, + interactionSource = interactionSource, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colors.onSurface, + backgroundColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + ) + } + } + } + } +} + +@Composable +fun BillAdditionalDetailsSection( + viewModel: EditBillViewModel, + categories: List, + paymentModes: List, + canEdit: Boolean +) { + var categoryExpanded by remember { mutableStateOf(false) } + val selectedCategory = + categories.find { it.remoteId.toInt() == viewModel.categoryRemoteId } + + EditableExposedDropdownMenu( + value = selectedCategory?.name ?: "", + placeholder = stringResource(R.string.setting_category), + expanded = categoryExpanded, + onExpandedChange = { categoryExpanded = it }, + onDismissRequest = { categoryExpanded = false }, + enabled = canEdit, + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedCategory != null) { + Text(text = selectedCategory.icon, fontSize = 20.sp) + } else { + Icon(Icons.Default.Category, contentDescription = null) + } + } + }, + content = { + DropdownMenuItem(onClick = { + viewModel.categoryRemoteId = 0 + categoryExpanded = false + }) { + Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.category_none)) + } + categories.forEach { category -> + DropdownMenuItem(onClick = { + viewModel.categoryRemoteId = category.remoteId.toInt() + categoryExpanded = false + }) { + Text(text = category.icon, fontSize = 20.sp) + Spacer(modifier = Modifier.width(12.dp)) + Text(category.name ?: "") + } + } + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + var pmExpanded by remember { mutableStateOf(false) } + val selectedPm = + paymentModes.find { it.remoteId.toInt() == viewModel.paymentModeRemoteId } + + EditableExposedDropdownMenu( + value = selectedPm?.name ?: "", + placeholder = stringResource(R.string.setting_payment_mode), + expanded = pmExpanded, + onExpandedChange = { pmExpanded = it }, + onDismissRequest = { pmExpanded = false }, + enabled = canEdit, + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedPm != null) { + Text(text = selectedPm.icon, fontSize = 20.sp) + } else { + Icon(Icons.Default.Payment, contentDescription = null) + } + } + }, + content = { + DropdownMenuItem(onClick = { + viewModel.paymentModeRemoteId = 0 + pmExpanded = false + }) { + Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.payment_mode_none)) + } + paymentModes.forEach { pm -> + DropdownMenuItem(onClick = { + viewModel.paymentModeRemoteId = pm.remoteId.toInt() + pmExpanded = false + }) { + Text(text = pm.icon, fontSize = 20.sp) + Spacer(modifier = Modifier.width(12.dp)) + Text(pm.name ?: "") + } + } + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val repeatOptions = listOf( + DBBill.NON_REPEATED to stringResource(R.string.repeat_no), + DBBill.REPEAT_DAY to stringResource(R.string.repeat_day), + DBBill.REPEAT_WEEK to stringResource(R.string.repeat_week), + DBBill.REPEAT_FORTNIGHT to stringResource(R.string.repeat_fortnight), + DBBill.REPEAT_MONTH to stringResource(R.string.repeat_month), + DBBill.REPEAT_YEAR to stringResource(R.string.repeat_year) + ) + var repeatExpanded by remember { mutableStateOf(false) } + val selectedRepeat = repeatOptions.find { it.first == viewModel.repeat } + + EditableExposedDropdownMenu( + value = selectedRepeat?.second ?: "", + placeholder = stringResource(R.string.setting_project_repetition), + expanded = repeatExpanded, + onExpandedChange = { repeatExpanded = it }, + onDismissRequest = { repeatExpanded = false }, + enabled = canEdit, + leadingIcon = { Icon(Icons.Default.Repeat, contentDescription = null) }, + content = { + repeatOptions.forEach { (value, label) -> + DropdownMenuItem(onClick = { + viewModel.repeat = value + repeatExpanded = false + }) { + Text(label) + } + } + } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = viewModel.comment, + onValueChange = { viewModel.comment = it }, + enabled = canEdit, + placeholder = { Text(stringResource(R.string.setting_comment)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = false, + leadingIcon = { + Icon( + Icons.AutoMirrored.Filled.Comment, + contentDescription = null + ) + } + ) +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview(showBackground = true) +@Composable +fun EditBillScreenPreview() { + MaterialTheme { + EditBillScreen( + viewModel = EditBillViewModel().apply { + what = "Pizza" + amount = "12.50" + members = listOf( + DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null), + DBMember(2, 0, 0, "Bob", true, 1.0, 0, null, null, null, null, null) + ) + }, + categories = emptyList(), + paymentModes = emptyList(), + onSave = {}, + onBack = {}, + onDateClick = {}, + onTimeClick = {}, + onScan = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt new file mode 100644 index 0000000..34f0592 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt @@ -0,0 +1,224 @@ +package net.helcel.cowspent.android.bill_edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.android.helper.DialogState +import net.helcel.cowspent.util.SupportUtil +import net.helcel.cowspent.util.evalMath + +import net.helcel.cowspent.model.DBCurrency +import androidx.compose.ui.graphics.vector.ImageVector + +class EditBillViewModel : ViewModel() { + var what by mutableStateOf("") + var amount by mutableStateOf("") + var comment by mutableStateOf("") + var timestamp by mutableLongStateOf(0L) + var payerId by mutableLongStateOf(0L) + var repeat by mutableStateOf(DBBill.NON_REPEATED) + var paymentModeRemoteId by mutableIntStateOf(0) + var categoryRemoteId by mutableIntStateOf(0) + + var currencies by mutableStateOf>(emptyList()) + var mainCurrencyName by mutableStateOf("") + var members by mutableStateOf>(emptyList()) + + var owersSelection = mutableStateMapOf() + var isCustomSplit by mutableStateOf(false) + var owersCustomSplit = mutableStateMapOf() + + var dialogState by mutableStateOf(null) + + val amountAsDouble: Double + get() { + val amountStr = amount.replace(',', '.') + return try { + if (amountStr.matches("[0-9.]+".toRegex())) { + amountStr.toDouble() + } else { + evalMath(amountStr) + } + } catch (_: Exception) { + 0.0 + } + } + + fun getEvenSplit(): Double { + val selectedOwersCount = owersSelection.count { it.value } + return if (selectedOwersCount > 0) amountAsDouble / selectedOwersCount else 0.0 + } + + fun updateSplits() { + if (!isCustomSplit) { + val even = getEvenSplit() + val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString() + members.forEach { m -> + if (owersSelection[m.id] == true) { + owersCustomSplit[m.id] = evenStr + } else { + owersCustomSplit.remove(m.id) + } + } + } + } + + fun toggleMember(id: Long, selected: Boolean) { + owersSelection[id] = selected + if (isCustomSplit) { + if (selected) { + if (owersCustomSplit[id].isNullOrEmpty()) { + owersCustomSplit[id] = "0" + } + } else { + owersCustomSplit.remove(id) + } + } else { + updateSplits() + } + } + + fun getDiffSplit(): Double { + val customTotal = owersCustomSplit.entries + .filter { owersSelection[it.key] == true } + .sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 } + return amountAsDouble - customTotal + } + + fun getOwersIds(): List { + return owersSelection.filter { it.value }.keys.toList() + } + + fun showDialog( + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: List? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null + ) { + dialogState = DialogState( + title = title, + message = message, + icon = icon, + items = items, + positiveText = positiveText, + negativeText = negativeText, + neutralText = neutralText, + onConfirm = onConfirm, + onCancel = onCancel, + onNeutral = onNeutral, + onItemSelected = onItemSelected + ) + } + + fun dismissDialog() { + dialogState = null + } + + fun convertCurrency(currency: DBCurrency) { + val originalAmountStr = amount + val originalAmount = amountAsDouble + if (originalAmount == 0.0) return + + val newAmount = originalAmount / currency.exchangeRate + amount = SupportUtil.round2(newAmount).toString() + + val currencyLabel = currency.name ?: "" + val conversionNote = "($originalAmountStr $currencyLabel)" + if (!comment.contains(conversionNote)) { + if (comment.isNotEmpty() && !comment.endsWith(" ")) { + comment += " " + } + comment += conversionNote + } + + if (isCustomSplit) { + owersCustomSplit.keys.toList().forEach { id -> + val value = owersCustomSplit[id] ?: "" + val partAmount = value.replace(',', '.').toDoubleOrNull() ?: 0.0 + if (partAmount != 0.0) { + val newPartAmount = partAmount / currency.exchangeRate + owersCustomSplit[id] = SupportUtil.round2(newPartAmount).toString() + } + } + } + + updateSplits() + } + + fun initFromBill(bill: DBBill, members: List, customSplits: Map? = null) { + this.members = members + what = bill.what + amount = if (bill.amount == 0.0) "" else bill.amount.toString() + comment = bill.comment ?: "" + timestamp = bill.timestamp + payerId = bill.payerId + repeat = bill.repeat ?: DBBill.NON_REPEATED + paymentModeRemoteId = bill.paymentModeRemoteId + categoryRemoteId = bill.categoryRemoteId + + owersSelection.clear() + owersCustomSplit.clear() + + if (customSplits != null) { + isCustomSplit = true + for (member in members) { + val selected = customSplits.containsKey(member.id) + owersSelection[member.id] = selected + if (selected) { + owersCustomSplit[member.id] = SupportUtil.round2(customSplits[member.id]!!).toString() + } + } + } else { + val billOwerIds = bill.billOwersIds + val selectedCount = billOwerIds.size + val evenSplit = if (selectedCount > 0) bill.amount / selectedCount else 0.0 + val evenSplitStr = if (evenSplit == 0.0) "" else SupportUtil.round2(evenSplit).toString() + + for (member in members) { + val selected = billOwerIds.contains(member.id) + owersSelection[member.id] = selected + if (selected) { + owersCustomSplit[member.id] = evenSplitStr + } + } + } + } + + fun isFormValid(): Boolean { + return what.isNotEmpty() && !what.contains(",") && + timestamp != 0L && + payerId != 0L && + owersSelection.any { it.value } + } + + fun getValidationError( + errorWhat: String, + errorDate: String, + errorPayer: String, + errorOwers: String, + errorInvalidForm: String + ): String? { + return when { + what.isEmpty() || what.contains(",") -> errorWhat + timestamp == 0L -> errorDate + payerId == 0L -> errorPayer + owersSelection.none { it.value } -> errorOwers + !isFormValid() -> errorInvalidForm + else -> null + } + } +} + diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt new file mode 100644 index 0000000..6345092 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt @@ -0,0 +1,85 @@ +package net.helcel.cowspent.android.bill_label + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.CategoryUtils +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.ProjectType + +class LabelBillsActivity : AppCompatActivity() { + private val viewModel: LabelBillsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, -1L) + if (projectId == -1L) { + finish() + return + } + + val db = CowspentSQLiteOpenHelper.getInstance(this) + + lifecycleScope.launch { + val (members, billsToLabel, categories, allCategorized) = withContext(Dispatchers.IO) { + val project = db.getProject(projectId) + val projectType = project?.type ?: ProjectType.LOCAL + + val members = db.getMembersOfProject(projectId, null) + val allBills = db.getBillsOfProject(projectId) + val billsToLabel = allBills.filter { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED } + val allCategorized = allBills.filter { it.categoryRemoteId != 0 && it.state != DBBill.STATE_DELETED } + + val syncedCategories = db.getCategories(projectId) + val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId) + val hardcoded = if (projectType == ProjectType.LOCAL) { + defaultCategories + } else { + listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT }) + } + val categories = syncedCategories + hardcoded + + Quadruple(members, billsToLabel, categories, allCategorized) + } + + viewModel.billsToLabel = billsToLabel + viewModel.categories = categories + viewModel.categoriesMap = categories.associateBy { it.remoteId } + viewModel.allCategorizedBills = allCategorized + viewModel.updateSuggestions() + + setContent { + ThemeUtils.CowspentTheme { + LabelBillsScreen( + viewModel = viewModel, + members = members, + db = db, + onBack = { finish() } + ) + } + } + } + } + + private data class Quadruple(val first: A, val second: B, val third: C, val fourth: D) + + companion object { + const val EXTRA_PROJECT_ID = "extra_project_id" + + fun createIntent(context: Context, projectId: Long): Intent { + return Intent(context, LabelBillsActivity::class.java).apply { + putExtra(EXTRA_PROJECT_ID, projectId) + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt new file mode 100644 index 0000000..cf1263a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt @@ -0,0 +1,269 @@ +package net.helcel.cowspent.android.bill_label + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.SupportUtil + +@Composable +fun LabelBillsScreen( + viewModel: LabelBillsViewModel, + members: List, + db: CowspentSQLiteOpenHelper, + onBack: () -> Unit +) { + val currentBill = viewModel.currentBill + val remainingCount = viewModel.billsToLabel.size - viewModel.currentBillIndex + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.label_bills_title)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (currentBill != null) { + BillSummaryCard(currentBill, members, remainingCount) + + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.label_bills_suggested), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + + val suggestions = viewModel.suggestedCategories + Box(modifier = Modifier.fillMaxWidth().height(64.dp), contentAlignment = Alignment.CenterStart) { + if (suggestions.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + suggestions.forEach { category -> + Box(modifier = Modifier.weight(1f)) { + CategoryButton( + icon = category.icon, + name = category.name ?: "", + onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) } + ) + } + } + repeat(2 - suggestions.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } else { + Text( + text = stringResource(R.string.label_bills_no_suggestions), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.setting_category), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(viewModel.categories) { category -> + CategoryButton( + icon = category.icon, + name = category.name ?: "", + onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) } + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { viewModel.skipCurrentBill() }, + modifier = Modifier.width(128.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray) + ) { + Text(stringResource(R.string.label_bills_skip)) + } + Spacer(modifier = Modifier.height(8.dp)) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(R.string.label_bills_no_more)) + } + } + } + } +} + +@Composable +fun BillSummaryCard(bill: DBBill, members: List, remainingCount: Int) { + val payerName = remember(bill.payerId, members) { + members.find { it.id == bill.payerId }?.name ?: bill.payerId.toString() + } + val owersNames = remember(bill.billOwersIds, members) { + bill.billOwersIds.joinToString(", ") { id -> + members.find { it.id == id }?.name ?: id.toString() + } + } + + Box { + Card( + elevation = 4.dp, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp, start = 8.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text(bill.what, maxLines = 2, style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold) + if (!bill.comment.isNullOrEmpty()) { + Text(bill.comment!!, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) + } + } + Text( + SupportUtil.normalNumberFormat.format(bill.amount), + style = MaterialTheme.typography.h5, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Divider() + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + text = "$payerName \u2192 $owersNames", + style = MaterialTheme.typography.caption, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = bill.date, + style = MaterialTheme.typography.caption + ) + } + } + } + + Surface( + modifier = Modifier + .size(28.dp), + shape = CircleShape, + color = MaterialTheme.colors.primary, + elevation = 6.dp + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = remainingCount.toString(), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +fun CategoryButton(icon: String, name: String, onClick: () -> Unit) { + OutlinedButton( + onClick = onClick, + modifier = Modifier.fillMaxWidth().height(60.dp), + contentPadding = PaddingValues(2.dp) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Text(icon, fontSize = 20.sp) + Text( + name, + fontSize = 11.sp, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview(showBackground = true) +@Composable +fun LabelBillsScreenPreview() { + val viewModel = LabelBillsViewModel().apply { + billsToLabel = listOf( + DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0, null, -1) + ) + val cats = listOf( + DBCategory(1, 1, 1, "Groceries", "🛒", ""), + DBCategory(2, 2, 1, "Leisure", "🥳", ""), + DBCategory(3, 3, 1, "Rent", "🏠", ""), + DBCategory(4, 4, 1, "Bills", "💸", "") + ) + categories = cats + categoriesMap = cats.associateBy { it.remoteId } + } + val members = listOf( + DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null), + DBMember(2L, 0, 1L, "Bob", true, 1.0, 0, 100, 255, 100, null, null) + ) + MaterialTheme { + LabelBillsScreen( + viewModel = viewModel, + members = members, + db = CowspentSQLiteOpenHelper.getInstance(LocalContext.current), + onBack = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt new file mode 100644 index 0000000..af27cc4 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt @@ -0,0 +1,101 @@ +package net.helcel.cowspent.android.bill_label + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper + +class LabelBillsViewModel : ViewModel() { + var billsToLabel by mutableStateOf>(emptyList()) + internal set + var currentBillIndex by mutableIntStateOf(0) + private set + var categories by mutableStateOf>(emptyList()) + internal set + var suggestedCategories by mutableStateOf>(emptyList()) + private set + + internal var categoriesMap: Map = emptyMap() + internal var allCategorizedBills: List = emptyList() + var onBillProcessed: ((Long) -> Unit)? = null + + val currentBill: DBBill? + get() = if (currentBillIndex < billsToLabel.size) billsToLabel[currentBillIndex] else null + + fun updateSuggestions() { + val bill = currentBill + if (bill == null) { + suggestedCategories = emptyList() + return + } + val name = bill.what.lowercase().trim() + if (name.isEmpty()) { + suggestedCategories = emptyList() + return + } + + val matches = allCategorizedBills.filter { + val otherName = it.what.lowercase().trim() + otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName)) + } + + val counts = matches.groupBy { it.categoryRemoteId } + .mapValues { it.value.size } + .toList() + .sortedByDescending { it.second } + .take(2) + + suggestedCategories = counts.mapNotNull { (catId, _) -> + categoriesMap[catId.toLong()] + } + } + + fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Int) { + currentBill?.let { bill -> + db.updateBillAndSync( + bill = bill, + newPayerId = bill.payerId, + newAmount = bill.amount, + newTimestamp = bill.timestamp, + newWhat = bill.what, + newOwersIds = bill.billOwersIds, + newRepeat = bill.repeat, + newPaymentMode = bill.paymentMode, + newPaymentModeRemoteId = bill.paymentModeRemoteId, + newCategoryId = categoryId, + newComment = bill.comment + ) + bill.categoryRemoteId = categoryId + onBillProcessed?.invoke(bill.id) + moveToNext() + } + } + + fun skipCurrentBill() { + currentBill?.let { bill -> + onBillProcessed?.invoke(bill.id) + } + moveToNext() + } + + private fun moveToNext() { + if (billsToLabel.isEmpty()) return + + val start = currentBillIndex + var next = (start + 1) % billsToLabel.size + while (next != start && billsToLabel[next].categoryRemoteId != 0) { + next = (next + 1) % billsToLabel.size + } + + currentBillIndex = if (billsToLabel[next].categoryRemoteId == 0) { + next + } else { + billsToLabel.size + } + updateSuggestions() + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt new file mode 100644 index 0000000..a0c711b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt @@ -0,0 +1,145 @@ +package net.helcel.cowspent.android.currencies + +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.showToast +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCurrency +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.ICallback + + +class ManageCurrenciesActivity : AppCompatActivity() { + + private val viewModel: ManageCurrenciesViewModel by viewModels() + private var db: CowspentSQLiteOpenHelper? = null + private var selectedProjectID: Long = -1 + + private val editMainCurrencyCallBack: ICallback = object : ICallback { + override fun onFinish() {} + override fun onFinish(result: String, message: String) { + if (message.isEmpty()) { + showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG) + } else { + viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message), + message=getString(R.string.currency_manager), + positiveText = getString(android.R.string.ok)) + } + } + override fun onScheduled() {} + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + intent.extras?.let { + selectedProjectID = it.getLong(EXTRA_PROJECT_ID) + } + if (selectedProjectID == -1L) { + Log.e(TAG, "Missing project id") + finish() + return + } + + db = CowspentSQLiteOpenHelper.getInstance(this) + + lifecycleScope.launch { + val project = withContext(Dispatchers.IO) { db!!.getProject(selectedProjectID) } + viewModel.mainCurrencyName = project?.currencyName?.let { if (it == "null") "" else it } ?: "" + updateCurrenciesList() + + setContent { + ThemeUtils.CowspentTheme { + ManageCurrenciesScreen( + viewModel = viewModel, + onBack = { finish() }, + onSaveMain = { saveMainCurrency() }, + onAdd = { addCurrency() }, + onDelete = { deleteCurrency(it) } + ) + } + } + } + } + + private fun saveMainCurrency() { + val newMainCurrencyName = viewModel.mainCurrencyName + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db!!.updateProject( + selectedProjectID, null, null, null, + null, null, newMainCurrencyName, + null, null, null + ) + val project = db!!.getProject(selectedProjectID) + if (project != null) { + db!!.syncIfRemote(project) + withContext(Dispatchers.Main) { + if (!db!!.cowspentServerSyncHelper + .editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack) + ) { + showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG) + } + } + } + } + } + } + + private fun addCurrency() { + val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 } + val newCurrency = DBCurrency( + 0, 0, selectedProjectID, + viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED + ) + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db!!.addCurrencyAndSync(newCurrency) + } + viewModel.newCurrencyName = "" + viewModel.newCurrencyRate = "" + updateCurrenciesList() + } + } + + private fun deleteCurrency(currency: DBCurrency) { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db!!.setCurrencyStateSync(currency.id, DBBill.STATE_DELETED) + } + updateCurrenciesList() + } + } + + private suspend fun updateCurrenciesList() { + val currenciesDB = withContext(Dispatchers.IO) { + val list = db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_ADDED).toMutableList() + list.addAll(db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_EDITED)) + list.addAll(db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_OK)) + list + } + withContext(Dispatchers.Main) { + viewModel.currencies = currenciesDB + } + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + companion object { + private val TAG = ManageCurrenciesActivity::class.java.simpleName + const val EXTRA_PROJECT_ID = "EXTRA_PROJECT_ID" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt new file mode 100644 index 0000000..346fe2a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -0,0 +1,178 @@ +package net.helcel.cowspent.android.currencies + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.AlertDialog +import net.helcel.cowspent.model.DBCurrency + +@Composable +fun ManageCurrenciesScreen( + viewModel: ManageCurrenciesViewModel, + onBack: () -> Unit, + onSaveMain: () -> Unit, + onAdd: () -> Unit, + onDelete: (DBCurrency) -> Unit +) { + val dialogState = viewModel.dialogState + if (dialogState != null) { + AlertDialog( + showDialog = true, + onDismissRequest = { viewModel.dismissDialog() }, + title = dialogState.title, + message = dialogState.message, + icon = dialogState.icon, + items = dialogState.items, + positiveText = dialogState.positiveText, + negativeText = dialogState.negativeText, + neutralText = dialogState.neutralText, + onConfirm = { + dialogState.onConfirm?.invoke() + viewModel.dismissDialog() + }, + onCancel = { + dialogState.onCancel?.invoke() + viewModel.dismissDialog() + }, + onNeutral = { + dialogState.onNeutral?.invoke() + viewModel.dismissDialog() + } + ) { + dialogState.onItemSelected?.invoke(it) + viewModel.dismissDialog() + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.currency_manager)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(16.dp) + .fillMaxSize() + ) { + Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6) + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = viewModel.mainCurrencyName, + onValueChange = { viewModel.mainCurrencyName = it }, + label = { Text(stringResource(R.string.currency_edit_name)) }, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) { + Text(stringResource(R.string.save_or_discard_bill_dialog_save)) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6) + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = viewModel.newCurrencyName, + onValueChange = { viewModel.newCurrencyName = it }, + label = { Text(stringResource(R.string.currency_edit_name)) }, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(8.dp)) + OutlinedTextField( + value = viewModel.newCurrencyRate, + onValueChange = { viewModel.newCurrencyRate = it }, + label = { Text(stringResource(R.string.currency_rate)) }, + modifier = Modifier.weight(1f) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = onAdd, + modifier = Modifier.fillMaxWidth(), + enabled = viewModel.isAddEnabled() + ) { + Text(stringResource(R.string.simple_add)) + } + + Spacer(modifier = Modifier.height(24.dp)) + + LazyColumn(modifier = Modifier.weight(1f)) { + items(viewModel.currencies) { currency -> + CurrencyRow(currency, onDelete = { onDelete(currency) }) + } + } + } + } +} + +@Composable +fun CurrencyRow(currency: DBCurrency, onDelete: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(currency.name ?: "", style = MaterialTheme.typography.subtitle1) + Text("Rate: ${currency.exchangeRate}", style = MaterialTheme.typography.caption) + } + IconButton(onClick = onDelete) { + Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error) + } + } + Divider() +} + +@Preview(showBackground = true) +@Composable +fun CurrencyRowPreview() { + MaterialTheme { + CurrencyRow( + currency = DBCurrency(1, 0, 0, "USD", 1.0, 0), + onDelete = {} + ) + } +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview(showBackground = true) +@Composable +fun ManageCurrenciesScreenPreview() { + MaterialTheme { + ManageCurrenciesScreen( + viewModel = ManageCurrenciesViewModel().apply { + mainCurrencyName = "EUR" + currencies = listOf( + DBCurrency(1, 0, 0, "USD", 1.1, 0), + DBCurrency(2, 0, 0, "GBP", 0.85, 0) + ) + }, + onBack = {}, + onSaveMain = {}, + onAdd = {}, + onDelete = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt new file mode 100644 index 0000000..f4e0002 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt @@ -0,0 +1,55 @@ +package net.helcel.cowspent.android.currencies + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.android.helper.DialogState +import net.helcel.cowspent.model.DBCurrency + +class ManageCurrenciesViewModel : ViewModel() { + var mainCurrencyName by mutableStateOf("") + var newCurrencyName by mutableStateOf("") + var newCurrencyRate by mutableStateOf("") + + var currencies by mutableStateOf>(emptyList()) + + var dialogState by mutableStateOf(null) + + fun showDialog( + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: List? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null + ) { + dialogState = DialogState( + title = title, + message = message, + icon = icon, + items = items, + positiveText = positiveText, + negativeText = negativeText, + neutralText = neutralText, + onConfirm = onConfirm, + onCancel = onCancel, + onNeutral = onNeutral, + onItemSelected = onItemSelected + ) + } + + fun dismissDialog() { + dialogState = null + } + + fun isAddEnabled(): Boolean { + return newCurrencyName.isNotEmpty() && newCurrencyRate.isNotEmpty() + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt b/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt new file mode 100644 index 0000000..a50188d --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt @@ -0,0 +1,350 @@ +package net.helcel.cowspent.android.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.UserAvatar +import net.helcel.cowspent.android.helper.formatBalance +import net.helcel.cowspent.android.helper.lazyVerticalScrollbar +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType + +@Composable +fun Drawer( + projects: List, + members: List, + memberBalances: Map = emptyMap(), + selectedProjectId: Long, + selectedMemberId: Long?, + lastSyncText: String, + showArchived: Boolean = false, + onProjectClick: (Long) -> Unit, + onProjectOptionsClick: (Long) -> Unit, + onMemberClick: (Long?) -> Unit, + onAddProjectClick: () -> Unit, + onAppSettingsClick: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.surface), + ) { + DrawerHeader( + lastSyncText = lastSyncText, + onAddProjectClick = onAddProjectClick, + onAppSettingsClick = onAppSettingsClick + ) + + BoxWithConstraints(modifier = Modifier.weight(1f)) { + val maxBottomHeight = maxHeight / 2 + + Column(modifier = Modifier.fillMaxSize()) { + // Projects Section + val activeProjects = projects.filter { !it.isArchived } + .sortedWith(compareByDescending { it.latestBillTs }.thenByDescending { it.id }) + val archivedProjects = projects.filter { it.isArchived }.sortedByDescending { it.archivedTs } + val archivedToDisplay = if (showArchived) archivedProjects else archivedProjects.filter { it.id == selectedProjectId } + + val projectsState = rememberLazyListState() + LazyColumn( + state = projectsState, + modifier = Modifier.weight(1f).lazyVerticalScrollbar(projectsState) + ) { + items(activeProjects) { project -> + ProjectDrawerItem( + project = project, + isSelected = project.id == selectedProjectId, + onClick = { onProjectClick(project.id) }, + onOptionsClick = { onProjectOptionsClick(project.id) } + ) + } + if (archivedToDisplay.isNotEmpty()) { + item { Divider(Modifier.height(4.dp)) } + items(archivedToDisplay) { project -> + ProjectDrawerItem( + project = project, + isSelected = project.id == selectedProjectId, + onClick = { onProjectClick(project.id) }, + onOptionsClick = { onProjectOptionsClick(project.id) }, + alpha = 0.6f, + icon = Icons.Default.Archive + ) + } + } + } + + Divider(Modifier.height(4.dp)) + + // Members Section + val membersState = rememberLazyListState() + LazyColumn( + state = membersState, + modifier = Modifier + .heightIn(max = maxBottomHeight) + .lazyVerticalScrollbar(membersState) + ) { + if (members.isNotEmpty()) { + item { + DrawerItem( + icon = Icons.Default.Receipt, + text = stringResource(R.string.label_all_bills), + selected = selectedMemberId == null, + onClick = { onMemberClick(null) } + ) + } + items(members) { member -> + val balance = memberBalances[member.id] ?: 0.0 + MemberDrawerItem( + member = member, + balance = balance, + isSelected = member.id == selectedMemberId, + onClick = { onMemberClick(member.id) } + ) + } + } + } + } + } + Spacer(Modifier.height(8.dp)) + } +} + +@Composable +private fun DrawerHeader( + lastSyncText: String, + onAddProjectClick: () -> Unit, + onAppSettingsClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.primary) + .padding(16.dp, 4.dp) + ) { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(id = R.string.app_name), + color = MaterialTheme.colors.onPrimary, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = onAddProjectClick, + modifier = Modifier.scale(0.8f) + ) { + Icon( + Icons.Default.Add, + contentDescription = stringResource(id = R.string.action_add_project), + tint = MaterialTheme.colors.onPrimary + ) + } + IconButton( + onClick = onAppSettingsClick, + modifier = Modifier.scale(0.8f) + ) { + Icon( + Icons.Default.Settings, + contentDescription = stringResource(id = R.string.action_settings), + tint = MaterialTheme.colors.onPrimary + ) + } + } + if (lastSyncText.isNotEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Default.Sync, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colors.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(lastSyncText, color = MaterialTheme.colors.onPrimary, fontSize = 12.sp) + } + } + } + } +} + +@Composable +private fun ProjectDrawerItem( + project: DBProject, + isSelected: Boolean, + onClick: () -> Unit, + onOptionsClick: () -> Unit, + alpha: Float = 1f, + icon: ImageVector = Icons.Default.Folder +) { + val projectName = project.name.ifEmpty { project.remoteId } + DrawerItem( + icon = icon, + text = projectName, + selected = isSelected, + onClick = onClick, + onSecondaryClick = onOptionsClick, + alpha = alpha + ) +} + +@Composable +private fun MemberDrawerItem( + member: DBMember, + balance: Double, + isSelected: Boolean, + onClick: () -> Unit +) { + val balanceText = formatBalance(balance) + val balanceColor = when { + balance > 0.01 -> Color(0xFF4CAF50) + balance < -0.01 -> Color(0xFFF44336) + else -> MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + } + + DrawerItem( + member = member, + balanceText = balanceText.ifEmpty { null }, + balanceColor = balanceColor, + selected = isSelected, + onClick = onClick + ) +} + +@Composable +fun DrawerItem( + icon: ImageVector? = null, + member: DBMember? = null, + text: String? = null, + balanceText: String? = null, + balanceColor: Color = Color.Unspecified, + selected: Boolean = false, + alpha: Float = 1f, + onClick: () -> Unit, + onSecondaryClick: (() -> Unit)? = null +) { + val backgroundColor = if (selected) MaterialTheme.colors.primary.copy(alpha = 0.12f) else Color.Transparent + val contentColor = (if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface).copy(alpha = alpha) + + Row( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = if (member != null) 0.dp else 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (member != null) { + UserAvatar( + name = member.name, + r = member.r, + g = member.g, + b = member.b, + avatar = member.avatar, + disabled = !member.isActivated, + size = 24.dp, + alpha = alpha + ) + } else if (icon != null) { + Icon(icon, contentDescription = null, tint = contentColor.copy(alpha = 0.6f * alpha)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + val itemText = text ?: member?.name ?: "" + Text( + text = itemText, + modifier = Modifier.weight(1f), + color = contentColor, + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis + ) + + if (balanceText != null) { + Text( + text = balanceText, + color = balanceColor, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(end = 8.dp) + ) + } + + if (onSecondaryClick != null) { + IconButton(onClick = onSecondaryClick) { + Icon( + Icons.Default.MoreVert, + contentDescription = null, + tint = contentColor.copy(alpha = 0.6f) + ) + } + } else if (member != null) { + // Equalize height with project items that have a secondary button + Spacer(modifier = Modifier.height(48.dp)) + } + } +} + +@Preview(showBackground = true) +@Composable +fun DrawerItemPreview() { + MaterialTheme { + DrawerItem( + icon = Icons.Default.Folder, + text = "My Project", + selected = true, + onClick = {}, + onSecondaryClick = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun DrawerPreview() { + MaterialTheme { + Drawer( + projects = listOf( + DBProject(1, "Vacation", "", "vacation", null, null, null, ProjectType.LOCAL, 0L, null, false, 0, null, null), + DBProject(2, "Home", "", "home", null, null, null, ProjectType.LOCAL, 0L, null, false, 0, null, 123456789L) + ), + members = listOf( + DBMember(1, 0, 1, "Alice", true, 1.0, 0, null, null, null, null, null), + DBMember(2, 0, 1, "Bob", true, 1.0, 0, null, null, null, null, null) + ), + selectedProjectId = 1, + selectedMemberId = null, + lastSyncText = "Last sync: 5 mins ago", + onProjectClick = {}, + onProjectOptionsClick = {}, + onMemberClick = {}, + onAddProjectClick = {}, + onAppSettingsClick = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt b/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt new file mode 100644 index 0000000..55ae77f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt @@ -0,0 +1,312 @@ +package net.helcel.cowspent.android.helper + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +data class DialogState( + val title: String? = null, + val message: String? = null, + val icon: ImageVector? = null, + val items: List? = null, + val itemIcons: List? = null, + val positiveText: String? = null, + val negativeText: String? = null, + val neutralText: String? = null, + val onConfirm: (() -> Unit)? = null, + val onCancel: (() -> Unit)? = null, + val onNeutral: (() -> Unit)? = null, + val onItemSelected: ((Int) -> Unit)? = null +) + +@Composable +fun StatefulAlertDialog( + state: DialogState?, + onDismissRequest: () -> Unit +) { + if (state != null) { + AlertDialog( + showDialog = true, + onDismissRequest = onDismissRequest, + title = state.title, + message = state.message, + icon = state.icon, + items = state.items, + itemIcons = state.itemIcons, + positiveText = state.positiveText, + negativeText = state.negativeText, + neutralText = state.neutralText, + onConfirm = { + state.onConfirm?.invoke() + onDismissRequest() + }, + onCancel = { + state.onCancel?.invoke() + onDismissRequest() + }, + onNeutral = { + state.onNeutral?.invoke() + onDismissRequest() + } + ) { + state.onItemSelected?.invoke(it) + onDismissRequest() + } + } +} + +@Composable +fun AlertDialog( + modifier: Modifier = Modifier, + showDialog: Boolean, + onDismissRequest: () -> Unit, + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: List? = null, + itemIcons: List? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null +) { + if (showDialog) { + AlertDialog( + onDismissRequest = onDismissRequest, + shape = MaterialTheme.shapes.large, + title = if (title != null || icon != null) { + { + Row(verticalAlignment = Alignment.CenterVertically) { + if (icon != null) { + Icon(icon, contentDescription = null) + Spacer(Modifier.width(8.dp)) + } + if (title != null) { + Text(text = title) + } + } + } + } else null, + text = if (message != null || items != null) { + { + Column { + if (message != null) { + Text(text = message) + } + if (items != null) { + if (message != null) Spacer(Modifier.height(8.dp)) + LazyColumn { + itemsIndexed(items) { index, item -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onItemSelected?.invoke(index) + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (itemIcons != null && index < itemIcons.size) { + Icon( + itemIcons[index], + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary.copy(alpha = 0.7f) + ) + Spacer(Modifier.width(16.dp)) + } + Text( + text = item.toString(), + style = MaterialTheme.typography.body1 + ) + } + } + } + } + } + } + } else null, + buttons = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = 8.dp), + horizontalArrangement = Arrangement.End + ) { + if (neutralText != null) { + TextButton(onClick = { onNeutral?.invoke() }) { + Text(neutralText) + } + Spacer(modifier = Modifier.weight(1f)) + } + if (negativeText != null) { + TextButton(onClick = { onCancel?.invoke() }) { + Text(negativeText) + } + } + if (positiveText != null) { + TextButton(onClick = { onConfirm?.invoke() }) { + Text(positiveText) + } + } + } + }, + modifier = modifier + ) + } +} + +@Composable +fun AlertDialogContent( + modifier: Modifier = Modifier, + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: Array? = null, + itemIcons: Array? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null +) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface), + modifier = modifier + ) { + Column(modifier = Modifier.padding(24.dp,16.dp)) { + if (title != null || icon != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 16.dp) + ) { + if (icon != null) { + Icon(icon, contentDescription = null) + Spacer(Modifier.width(8.dp)) + } + if (title != null) { + Text(text = title, style = MaterialTheme.typography.h6) + } + } + } + + if (message != null || items != null) { + Column { + if (message != null) { + Text(text = message, style = MaterialTheme.typography.body1) + } + if (items != null) { + if (message != null) Spacer(Modifier.height(8.dp)) + LazyColumn { + itemsIndexed(items) { index, item -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onItemSelected?.invoke(index) + } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (itemIcons != null && index < itemIcons.size) { + Icon( + itemIcons[index], + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary.copy(alpha = 0.7f) + ) + Spacer(Modifier.width(16.dp)) + } + Text( + text = item.toString(), + style = MaterialTheme.typography.body1 + ) + } + } + } + } + } + } + + if (positiveText != null || negativeText != null || neutralText != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + if (neutralText != null) { + TextButton(onClick = { onNeutral?.invoke() }) { + Text(neutralText) + } + Spacer(modifier = Modifier.weight(1f)) + } + if (negativeText != null) { + TextButton(onClick = { onCancel?.invoke() }) { + Text(negativeText) + } + } + if (positiveText != null) { + TextButton(onClick = { onConfirm?.invoke() }) { + Text(positiveText) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun SimpleAlertDialogPreview() { + MaterialTheme { + AlertDialogContent( + title = "Info", + message = "This is a simple alert message.", + positiveText = "OK" + ) + } +} + +@Preview(showBackground = true) +@Composable +fun ConfirmationDialogPreview() { + MaterialTheme { + AlertDialogContent( + title = "Confirm Action", + message = "Are you sure you want to proceed?", + icon = Icons.Default.Info, + positiveText = "Yes", + negativeText = "No" + ) + } +} + +@Preview(showBackground = true) +@Composable +fun ListDialogWithIconsPreview() { + MaterialTheme { + AlertDialogContent( + title = "Select Option", + items = arrayOf("Edit", "Delete", "Share"), + itemIcons = arrayOf(Icons.Default.Edit, Icons.Default.Delete, Icons.Default.Share), + negativeText = "Cancel" + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt b/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt new file mode 100644 index 0000000..12855ad --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt @@ -0,0 +1,298 @@ +package net.helcel.cowspent.android.helper + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.graphics.ColorUtils +import kotlin.math.* +import androidx.core.graphics.toColorInt + + +private fun mLCHtoRBG(l: Float, c: Float, h: Float) : Int{ + val hRad = h * PI / 180 + val a = c * cos(hRad) + val b = c * sin(hRad) + return ColorUtils.LABToColor(l.toDouble(), a, b) +} + + +@Composable +fun ColorPicker( + initialColor: Int, + onColorChanged: (Int) -> Unit +) { + // LCH state + val initialLch = remember(initialColor) { + val lab = DoubleArray(3) + ColorUtils.colorToLAB(initialColor, lab) + val l = lab[0].toFloat() + val c = sqrt(lab[1].pow(2) + lab[2].pow(2)).toFloat() + val h = (atan2(lab[2], lab[1]) * 180 / PI).toFloat().let { if (it < 0) it + 360f else it } + floatArrayOf(l, c, h) + } + + var lightness by remember { mutableFloatStateOf(initialLch[0]) } + var chroma by remember { mutableFloatStateOf(initialLch[1]) } + var hue by remember { mutableFloatStateOf(initialLch[2]) } + + val currentColorInt = remember(lightness, chroma, hue) { + val color = mLCHtoRBG(lightness,chroma,hue) + onColorChanged(color) + color + } + + val currentColor = Color(currentColorInt) + + // HEX state + var hexText by remember { mutableStateOf("%06X".format(0xFFFFFF and currentColorInt)) } + var isHexValid by remember { mutableStateOf(true) } + val interactionSource = remember { MutableInteractionSource() } + + // Sync HEX with LCH + LaunchedEffect(currentColorInt) { + val newHex = "%06X".format(0xFFFFFF and currentColorInt) + if (hexText.uppercase() != newHex) { + hexText = newHex + isHexValid = true + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Preview Area + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(64.dp) + .shadow(4.dp, CircleShape) + .clip(CircleShape) + .background(currentColor) + .border(2.dp, Color.White, CircleShape) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = "Selected Color", + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + + BasicTextField( + value = hexText, + onValueChange = { newText -> + val filtered = newText.removePrefix("#").filter { it.isDigit() || it.uppercaseChar() in 'A'..'F' }.take(6) + hexText = filtered + if (filtered.length == 6) { + try { + val parsedColor = "#$filtered".toColorInt() + val lab = DoubleArray(3) + ColorUtils.colorToLAB(parsedColor, lab) + lightness = lab[0].toFloat() + chroma = sqrt(lab[1].pow(2) + lab[2].pow(2)).toFloat() + hue = (atan2(lab[2], lab[1]) * 180 / PI).toFloat().let { if (it < 0) it + 360f else it } + isHexValid = true + } catch (_: Exception) { + isHexValid = false + } + } else { + isHexValid = false + } + }, + modifier = Modifier + .width(104.dp) + .heightIn(min = 36.dp), // 1. Bypasses the default 56.dp constraint + textStyle = MaterialTheme.typography.body1.copy( + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colors.onSurface // Essential for BasicTextField visibility + ), + visualTransformation = { text -> + TransformedText( + AnnotatedString("#" + text.text), + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int = offset + 1 + override fun transformedToOriginal(offset: Int): Int = if (offset < 1) 0 else offset - 1 + } + ) + }, + singleLine = true, + interactionSource = interactionSource + ) { innerTextField -> + @OptIn(ExperimentalMaterialApi::class) + TextFieldDefaults.TextFieldDecorationBox( + value = hexText, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = { text -> + TransformedText( + AnnotatedString("#" + text.text), + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int = + offset + 1 + + override fun transformedToOriginal(offset: Int): Int = + if (offset < 1) 0 else offset - 1 + } + ) + }, + interactionSource = interactionSource, + isError = !isHexValid, // 2. Tells decoration box to draw the red error indicator line + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + focusedIndicatorColor = currentColor, + unfocusedIndicatorColor = MaterialTheme.colors.onSurface.copy(alpha = 0.3f) + ), + // 3. Reduces the vertical and horizontal internal padding + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + val hueBrush = remember { + Brush.horizontalGradient( + listOf( + Color.Red, Color.Yellow, Color.Green, Color.Cyan, Color.Blue, Color.Magenta, Color.Red + ) + ) + } + LchSlider( + label = "H", + value = hue, + range = 0f..360f, + onValueChange = { hue = it }, + brush = hueBrush + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LchSlider( + label = "C", + value = chroma, + range = 0f..150f, + onValueChange = { chroma = it }, + brush = Brush.horizontalGradient(listOf( + Color(mLCHtoRBG(lightness,0f, hue)), + Color(mLCHtoRBG(lightness,150f,hue)))) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // LCH Sliders + LchSlider( + label = "L", + value = lightness, + range = 0f..100f, + onValueChange = { lightness = it }, + brush = Brush.horizontalGradient(listOf(Color.Black, Color.White)) + ) + } +} + +@Composable +fun LchSlider( + label: String, + value: Float, + range: ClosedFloatingPointRange, + onValueChange: (Float) -> Unit, + brush: Brush +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = label, style = MaterialTheme.typography.subtitle2) + Spacer(Modifier.width(Dp(8f))) + Box( + modifier = Modifier + .fillMaxWidth() + .height(28.dp) + .shadow(2.dp, RoundedCornerShape(14.dp)) + .clip(RoundedCornerShape(14.dp)) + .background(brush) + .pointerInput(Unit) { + detectTapGestures { offset -> + val newValue = (offset.x / size.width).coerceIn( + 0f, + 1f + ) * (range.endInclusive - range.start) + range.start + onValueChange(newValue) + } + } + .pointerInput(Unit) { + detectDragGestures { change, _ -> + val newValue = (change.position.x / size.width).coerceIn( + 0f, + 1f + ) * (range.endInclusive - range.start) + range.start + onValueChange(newValue) + } + } + ) { + // Handle + Canvas(modifier = Modifier.fillMaxSize()) { + val fraction = (value - range.start) / (range.endInclusive - range.start) + val x = fraction * size.width + drawCircle( + color = Color.White, + radius = 12.dp.toPx(), + center = Offset(x, size.height / 2), + style = Stroke(width = 3.dp.toPx()) + ) + drawCircle( + color = Color.Black.copy(alpha = 0.2f), + radius = 13.dp.toPx(), + center = Offset(x, size.height / 2), + style = Stroke(width = 1.dp.toPx()) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun ColorPickerPreview() { + MaterialTheme { + ColorPicker(initialColor = android.graphics.Color.BLUE, onColorChanged = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/CommonComposables.kt b/app/src/main/java/net/helcel/cowspent/android/helper/CommonComposables.kt new file mode 100644 index 0000000..89b325f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/CommonComposables.kt @@ -0,0 +1,85 @@ +package net.helcel.cowspent.android.helper + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ClickableOutlinedTextField( + value: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null +) { + Box(modifier = modifier) { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + enabled = enabled, + placeholder = placeholder, + modifier = Modifier.fillMaxWidth(), + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + Box( + modifier = Modifier + .matchParentSize() + .clickable(enabled = enabled) { onClick() }) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun EditableExposedDropdownMenu( + value: String, + placeholder: String, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, + colors: TextFieldColors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + content: @Composable ColumnScope.() -> Unit +) { + ExposedDropdownMenuBox( + expanded = expanded && enabled, + onExpandedChange = { if (enabled) onExpandedChange(!expanded) }, + modifier = modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + enabled = enabled, + placeholder = { Text(placeholder) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = leadingIcon, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = colors + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest + ) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 280.dp) + .verticalScrollbar(scrollState) + .verticalScroll(scrollState), + content = content + ) + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt b/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt new file mode 100644 index 0000000..ae57271 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt @@ -0,0 +1,20 @@ +package net.helcel.cowspent.android.helper + +import net.helcel.cowspent.util.SupportUtil +import java.util.Locale +import kotlin.math.abs +import kotlin.math.round + +fun formatShortValue(value: Double): String { + return when { + value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "") + value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "") + else -> String.format(Locale.ROOT, "%.0f", value) + } +} + +fun formatBalance(balance: Double): String { + val rbalance = round(abs(balance) * 100.0) / 100.0 + val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else "" + return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}" +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/ModifierUtils.kt b/app/src/main/java/net/helcel/cowspent/android/helper/ModifierUtils.kt new file mode 100644 index 0000000..e873649 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/ModifierUtils.kt @@ -0,0 +1,70 @@ +package net.helcel.cowspent.android.helper + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +fun Modifier.verticalScrollbar( + state: ScrollState, + width: Dp = 4.dp, + color: Color = Color.Gray, + alpha: Float = 0.6f +): Modifier = drawWithContent { + drawContent() + val maxValue = state.maxValue.toFloat() + if (maxValue > 0) { + val viewPortHeight = size.height + val totalContentHeight = maxValue + viewPortHeight + val scrollBarHeight = (viewPortHeight * viewPortHeight / totalContentHeight).coerceAtLeast(24.dp.toPx()) + val scrollBarOffset = (state.value.toFloat() / maxValue) * (viewPortHeight - scrollBarHeight) + + drawRoundRect( + color = color, + topLeft = Offset(size.width - width.toPx() - 2.dp.toPx(), scrollBarOffset), + size = Size(width.toPx(), scrollBarHeight), + cornerRadius = CornerRadius(width.toPx() / 2, width.toPx() / 2), + alpha = alpha + ) + } +} + +fun Modifier.lazyVerticalScrollbar( + state: LazyListState, + width: Dp = 4.dp, + color: Color = Color.Gray, + alpha: Float = 0.6f +): Modifier = drawWithContent { + drawContent() + val layoutInfo = state.layoutInfo + val visibleItemsInfo = layoutInfo.visibleItemsInfo + if (visibleItemsInfo.isNotEmpty()) { + val totalItemsCount = layoutInfo.totalItemsCount + val firstVisibleItem = visibleItemsInfo.first() + + val viewPortHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val avgItemHeight = visibleItemsInfo.map { it.size }.average().toFloat() + val totalContentHeight = avgItemHeight * totalItemsCount + + if (totalContentHeight > viewPortHeight) { + val scrollBarHeight = (viewPortHeight.toFloat() * viewPortHeight / totalContentHeight).coerceAtLeast(24.dp.toPx()) + val firstItemOffset = firstVisibleItem.offset + val scrollOffset = firstVisibleItem.index * avgItemHeight - firstItemOffset + val scrollBarOffset = (scrollOffset / (totalContentHeight - viewPortHeight)) * (viewPortHeight - scrollBarHeight) + + drawRoundRect( + color = color, + topLeft = Offset(size.width - width.toPx() - 2.dp.toPx(), scrollBarOffset), + size = Size(width.toPx(), scrollBarHeight), + cornerRadius = CornerRadius(width.toPx() / 2, width.toPx() / 2), + alpha = alpha + ) + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt new file mode 100644 index 0000000..2c87df7 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt @@ -0,0 +1,226 @@ +package net.helcel.cowspent.android.helper + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.util.Log +import androidx.activity.compose.setContent +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import net.helcel.cowspent.theme.ThemeUtils +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.zxing.* +import com.google.zxing.common.HybridBinarizer +import net.helcel.cowspent.R +import net.helcel.cowspent.android.main.MainConstants +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class QrCodeScannerActivity : AppCompatActivity() { + private lateinit var cameraExecutor: ExecutorService + private var isScanning = true + + override fun onCreate(state: Bundle?) { + super.onCreate(state) + + cameraExecutor = Executors.newSingleThreadExecutor() + + setContent { + ThemeUtils.CowspentTheme { + QrCodeScannerScreen( + onBack = { finish() }, + onResult = { handleResult(it) }, + cameraExecutor = cameraExecutor, + isScanning = isScanning, + onRequestPermission = { + ActivityCompat.requestPermissions( + this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS + ) + }, + hasPermission = allPermissionsGranted() + ) + } + } + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED + } + + override fun onDestroy() { + super.onDestroy() + cameraExecutor.shutdown() + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_CODE_PERMISSIONS) { + if (allPermissionsGranted()) { + // Permissions granted, the UI will recompose and start camera + } else { + finish() + } + } + } + + private fun handleResult(result: Result) { + if (!isScanning) return + isScanning = false + + Log.v(TAG, "QR result " + result.text) + + val intent = Intent() + intent.putExtra(MainConstants.KEY_QR_CODE, result.text) + setResult(RESULT_OK, intent) + finish() + } + + companion object { + private val TAG = QrCodeScannerActivity::class.java.simpleName + private const val REQUEST_CODE_PERMISSIONS = 10 + private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) + } +} + +@Composable +fun QrCodeScannerScreen( + onBack: () -> Unit, + onResult: (Result) -> Unit, + cameraExecutor: ExecutorService, + isScanning: Boolean, + onRequestPermission: () -> Unit, + hasPermission: Boolean +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.scan_qrcode)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.surface, + elevation = 0.dp + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + if (hasPermission) { + AndroidView( + factory = { ctx -> + PreviewView(ctx).apply { + scaleType = PreviewView.ScaleType.FILL_CENTER + } + }, + modifier = Modifier.fillMaxSize(), + update = { previewView -> + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val preview = androidx.camera.core.Preview.Builder().build().also { + it.surfaceProvider = previewView.surfaceProvider + } + + val imageAnalyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(cameraExecutor, QrCodeAnalyzer { result -> + if (isScanning) { + onResult(result) + } + }) + } + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + CameraSelector.DEFAULT_BACK_CAMERA, + preview, + imageAnalyzer + ) + } catch (exc: Exception) { + Log.e("QrCodeScannerScreen", "Use case binding failed", exc) + } + }, ContextCompat.getMainExecutor(context)) + } + ) + } else { + LaunchedEffect(Unit) { + onRequestPermission() + } + } + } + } +} + +private class QrCodeAnalyzer(private val onQrCodeScanned: (Result) -> Unit) : ImageAnalysis.Analyzer { + private val reader = MultiFormatReader().apply { + setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))) + } + + @OptIn(ExperimentalGetImage::class) + override fun analyze(image: ImageProxy) { + val buffer = image.planes[0].buffer + val data = ByteArray(buffer.remaining()) + buffer.get(data) + + val source = PlanarYUVLuminanceSource( + data, image.width, image.height, 0, 0, image.width, image.height, false + ) + val binarizer = HybridBinarizer(source) + val binaryBitmap = BinaryBitmap(binarizer) + + try { + val result = reader.decode(binaryBitmap) + onQrCodeScanned(result) + } catch (_: Exception) { + // No QR code found + } finally { + image.close() + } + } +} + +@Preview(showBackground = true) +@Composable +fun QrCodeScannerScreenPreview() { + MaterialTheme { + QrCodeScannerScreen( + onBack = {}, + onResult = {}, + cameraExecutor = Executors.newSingleThreadExecutor(), + isScanning = true, + onRequestPermission = {}, + hasPermission = true + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt b/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt new file mode 100644 index 0000000..d465e58 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt @@ -0,0 +1,219 @@ +package net.helcel.cowspent.android.helper + +import android.graphics.* +import android.graphics.drawable.Drawable +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.* +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.sqrt + +/** + * A Drawable object that draws text (1 character) on top of a circular/filled background. + */ +class TextDrawable private constructor( + private val mText: String, + r: Int, + g: Int, + b: Int, + private val mRadius: Float, + private val mDisabled: Boolean +) : Drawable() { + private val mTextPaint: Paint = Paint() + private val mBackground: Paint = Paint() + private val mDisabledCircle: Paint = Paint() + + init { + mBackground.style = Paint.Style.FILL + mBackground.isAntiAlias = true + mBackground.color = Color.rgb(r, g, b) + + if ((r + g + b) / 3 < 220) { + mTextPaint.color = Color.WHITE + } else { + mTextPaint.color = Color.BLACK + } + mTextPaint.textSize = mRadius + mTextPaint.isAntiAlias = true + mTextPaint.textAlign = Paint.Align.CENTER + + mDisabledCircle.style = Paint.Style.STROKE + mDisabledCircle.strokeWidth = mRadius * 0.2f + mDisabledCircle.isAntiAlias = true + mDisabledCircle.color = Color.DKGRAY + } + + override fun draw(canvas: Canvas) { + canvas.drawCircle(mRadius, mRadius, mRadius, mBackground) + canvas.drawText( + mText, + mRadius, + mRadius - (mTextPaint.descent() + mTextPaint.ascent()) / 2, + mTextPaint + ) + if (mDisabled) { + canvas.drawCircle(mRadius, mRadius, mRadius * 0.9f, mDisabledCircle) + canvas.drawLine( + mRadius * 0.4f, + mRadius * 1.6f, + mRadius * 1.6f, + mRadius * 0.4f, + mDisabledCircle + ) + } + } + + override fun setAlpha(alpha: Int) { + mTextPaint.alpha = alpha + } + + override fun setColorFilter(cf: ColorFilter?) { + mTextPaint.colorFilter = cf + } + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + companion object { + private const val INDEX_RED = 0 + private const val INDEX_GREEN = 1 + private const val INDEX_BLUE = 2 + private const val INDEX_HUE = 0 + private const val INDEX_SATURATION = 1 + private const val INDEX_LUMINATION = 2 + + fun getColorFromName(name: String): Int { + return try { + val hsl = calculateHSL(name) + val rgb = hslToRgb(hsl[0].toFloat(), hsl[1].toFloat(), hsl[2].toFloat(), 1f) + Color.rgb(rgb[0], rgb[1], rgb[2]) + } catch (_: NoSuchAlgorithmException) { + Color.WHITE + } + } + + @Throws(NoSuchAlgorithmException::class) + private fun calculateHSL(name: String): IntArray { + val result = arrayOf("0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0") + val rgb = doubleArrayOf(0.0, 0.0, 0.0) + var sat = 70 + val lum = 68 + val modulo = 16 + + var hash = name.lowercase(Locale.ROOT).replace("[^0-9a-f]".toRegex(), "") + if (!hash.matches("^[0-9a-f]{32}$".toRegex())) { + hash = md5(hash) + } + + for (i in hash.indices) { + result[i % modulo] = (result[i % modulo].toInt() + hash.substring(i, i + 1).toInt(16)).toString() + } + + for (count in 1 until modulo) { + rgb[count % 3] += result[count].toDouble() + } + + rgb[INDEX_RED] = rgb[INDEX_RED] % 255 + rgb[INDEX_GREEN] = rgb[INDEX_GREEN] % 255 + rgb[INDEX_BLUE] = rgb[INDEX_BLUE] % 255 + + val hsl = rgbToHsl(rgb[INDEX_RED], rgb[INDEX_GREEN], rgb[INDEX_BLUE]) + + val bright = sqrt( + 0.299 * rgb[INDEX_RED].pow(2.0) + 0.587 * rgb[INDEX_GREEN].pow(2.0) + 0.114 * rgb[INDEX_BLUE].pow(2.0) + ) + + if (bright >= 200) { + sat = 60 + } + + return intArrayOf((hsl[INDEX_HUE] * 360).toInt(), sat, lum) + } + + private fun hslToRgb(hParam: Float, sParam: Float, lParam: Float, alpha: Float): IntArray { + var h = hParam + var s = sParam + var l = lParam + if (s !in 0.0f..100.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Saturation") + } + if (l !in 0.0f..100.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Luminance") + } + if (alpha !in 0.0f..1.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Alpha") + } + + h %= 360.0f + h /= 360f + s /= 100f + l /= 100f + + val q = if (l < 0.5) { + l * (1 + s) + } else { + (l + s) - s * l + } + val p = 2 * l - q + val r = round(max(0f, hueToRgb(p, q, h + 1.0f / 3.0f)) * 256).toInt() + val g = round(max(0f, hueToRgb(p, q, h)) * 256).toInt() + val b = round(max(0f, hueToRgb(p, q, h - 1.0f / 3.0f)) * 256).toInt() + return intArrayOf(r, g, b) + } + + private fun hueToRgb(p: Float, q: Float, hParam: Float): Float { + var h = hParam + if (h < 0) h += 1f + if (h > 1) h -= 1f + if (6 * h < 1) return p + (q - p) * 6 * h + if (2 * h < 1) return q + if (3 * h < 2) return p + (q - p) * 6 * (2.0f / 3.0f - h) + return p + } + + private fun rgbToHsl(rUntrimmed: Double, gUntrimmed: Double, bUntrimmed: Double): DoubleArray { + val r = rUntrimmed / 255 + val g = gUntrimmed / 255 + val b = bUntrimmed / 255 + val max = max(r, max(g, b)) + val min = r.coerceAtMost(g.coerceAtMost(b)) + var h = (max + min) / 2 + val s: Double + val l = (max + min) / 2 + if (max == min) { + s = 0.0 + h = s // achromatic + } else { + val d = max - min + s = if (l > 0.5) d / (2 - max - min) else d / (max + min) + when (max) { + r -> { + h = (g - b) / d + (if (g < b) 6 else 0) + } + g -> { + h = (b - r) / d + 2 + } + b -> { + h = (r - g) / d + 4 + } + } + h /= 6.0 + } + val hsl = DoubleArray(3) + hsl[INDEX_HUE] = h + hsl[INDEX_SATURATION] = s + hsl[INDEX_LUMINATION] = l + return hsl + } + + @Throws(NoSuchAlgorithmException::class) + private fun md5(string: String): String { + val md5 = MessageDigest.getInstance("MD5").digest(string.toByteArray()) + return md5.joinToString("") { "%02x".format(it) } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/Toaster.kt b/app/src/main/java/net/helcel/cowspent/android/helper/Toaster.kt new file mode 100644 index 0000000..a57bf12 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/Toaster.kt @@ -0,0 +1,9 @@ +package net.helcel.cowspent.android.helper + +import android.content.Context +import android.widget.Toast + + +fun showToast(ctx: Context, text: CharSequence?, duration: Int = Toast.LENGTH_LONG) { + Toast.makeText(ctx, text, duration).show() +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/UserAvatar.kt b/app/src/main/java/net/helcel/cowspent/android/helper/UserAvatar.kt new file mode 100644 index 0000000..30bd1c8 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/helper/UserAvatar.kt @@ -0,0 +1,110 @@ +package net.helcel.cowspent.android.helper + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import android.graphics.BitmapFactory +import android.util.Base64 + +@Composable +fun UserAvatar( + name: String, + modifier: Modifier = Modifier, + size: Dp = 40.dp, + r: Int? = null, + g: Int? = null, + b: Int? = null, + avatar: String? = null, + disabled: Boolean = false, + alpha: Float = 1f +) { + val bitmap = remember(avatar) { + if (!avatar.isNullOrEmpty()) { + try { + val bytes = Base64.decode(avatar, Base64.DEFAULT) + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + } catch (_: Exception) { + null + } + } else { + null + } + } + + if (bitmap != null) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = name, + modifier = modifier + .size(size) + .clip(CircleShape), + contentScale = ContentScale.Crop, + alpha = alpha + ) + } else { + val backgroundColor = remember(name, r, g, b) { + if (r != null && g != null && b != null) { + Color(r, g, b) + } else { + Color(TextDrawable.getColorFromName(name)) + } + } + + val initials = name.take(1).uppercase() + val isLight = remember(backgroundColor) { + // Simple luminance check + val luminance = + 0.2126 * backgroundColor.red + 0.7152 * backgroundColor.green + 0.0722 * backgroundColor.blue + luminance > 0.5 + } + + Box( + modifier = modifier + .size(size) + .clip(CircleShape) + .background((if (disabled) Color.Gray else backgroundColor).copy(alpha = alpha)), + contentAlignment = Alignment.Center + ) { + Text( + text = initials, + color = (if (isLight) Color.Black else Color.White).copy(alpha = alpha), + fontSize = (size.value * 0.5).sp, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Preview +@Composable +fun UserAvatarPreview() { + UserAvatar(name = "Alice") +} + +@Preview +@Composable +fun UserAvatarCustomColorPreview() { + UserAvatar(name = "Bob", r = 255, g = 0, b = 0) +} + +@Preview +@Composable +fun UserAvatarDisabledPreview() { + UserAvatar(name = "Charlie", disabled = true) +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt new file mode 100644 index 0000000..cb855b2 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt @@ -0,0 +1,163 @@ +package net.helcel.cowspent.android.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Receipt +import androidx.compose.material.icons.filled.Repeat +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.UserAvatar +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.util.SupportUtil + +@Composable +fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(R.string.no_projects_title), style = MaterialTheme.typography.h6) + Spacer(modifier = Modifier.height(8.dp)) + Text(stringResource(R.string.no_projects_text)) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onConfigureNextcloud) { + Text(stringResource(R.string.configure_account_choice)) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onAddManually) { + Text(stringResource(R.string.add_project_choice)) + } + } +} + +@Composable +fun EmptyMembersState() { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(R.string.no_members_title), style = MaterialTheme.typography.h6) + Spacer(modifier = Modifier.height(8.dp)) + Text(stringResource(R.string.no_members_text)) + } +} + +@Composable +fun EmptyBillsState() { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6) + Spacer(modifier = Modifier.height(8.dp)) + Text(stringResource(R.string.no_bills_text)) + } +} + +@Composable +fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) { + Row { + Spacer(Modifier.width(16.dp)) + Divider(thickness = 1.dp, modifier=Modifier.width(40.dp)) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp, 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (payer != null) { + Box { + UserAvatar( + name = payer.name, + r = payer.r, + g = payer.g, + b = payer.b, + avatar = payer.avatar, + disabled = !payer.isActivated, + size = 40.dp + ) + if (bill.repeat != null && bill.repeat != DBBill.NON_REPEATED) { + Icon( + Icons.Default.Repeat, + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .size(24.dp) + .offset((-8).dp) + .background(MaterialTheme.colors.onSurface, CircleShape) + .padding(2.dp), + tint = MaterialTheme.colors.surface + ) + } + } + } else { + Icon( + Icons.Default.Receipt, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colors.primary + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(bill.formattedWhat.ifEmpty { bill.what }, fontWeight = FontWeight.Bold) + Text( + text = bill.formattedSubtitle.ifEmpty { bill.comment ?: "" }, + style = MaterialTheme.typography.caption, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Column(horizontalAlignment = Alignment.End) { + Text( + text = SupportUtil.normalNumberFormat.format(bill.amount), + style = MaterialTheme.typography.h6, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Composable +fun SectionHeader(title: String) { + Surface( + color = MaterialTheme.colors.background, + modifier = Modifier.fillMaxWidth() + ) { + Divider(thickness = 2.dp) + Text( + text = title, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.caption.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colors.primary + ) + } +} + +@Composable +fun EmptyState() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6) + Text(stringResource(R.string.no_bills_text), modifier = Modifier.padding(16.dp)) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt new file mode 100644 index 0000000..283f037 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt @@ -0,0 +1,586 @@ +package net.helcel.cowspent.android.main + +import android.annotation.SuppressLint +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Category +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.preference.PreferenceManager +import kotlinx.coroutines.launch +import net.helcel.cowspent.R +import net.helcel.cowspent.android.drawer.Drawer +import net.helcel.cowspent.android.helper.StatefulAlertDialog +import net.helcel.cowspent.android.project.ProjectOptionsDialogContent +import net.helcel.cowspent.android.project.ProjectShareDialogContent +import net.helcel.cowspent.android.project.member.MemberAddDialogContent +import net.helcel.cowspent.android.project.member.MemberEditDialogContent +import net.helcel.cowspent.android.project.member.MemberManagementDialogContent +import net.helcel.cowspent.android.project.settle.ProjectSettlementDialogContent +import net.helcel.cowspent.android.statistics.ProjectStatisticsActivity +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.SectionItem +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.IRefreshBillsListCallback + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BillsListScreen( + viewModel: BillsListViewModel, + db: CowspentSQLiteOpenHelper, + refreshCallback: IRefreshBillsListCallback, + onAddBillClick: () -> Unit, + onBillClick: (DBBill) -> Unit, + onProjectClick: (Long) -> Unit, + onProjectOptionsClick: (Long) -> Unit, + onProjectAction: (Long, Int) -> Unit, + onAccountSwitcherClick: () -> Unit, + onAddProjectClick: () -> Unit, + onAppSettingsClick: () -> Unit, + onLabelBillsClick: () -> Unit, + onRefresh: () -> Unit +) { + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + var isSearchExpanded by remember { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(isSearchExpanded) { + if (isSearchExpanded) { + focusRequester.requestFocus() + } + } + + LaunchedEffect(viewModel.searchQuery) { + refreshCallback.refreshLists(false) + } + + val pullRefreshState = rememberPullRefreshState(viewModel.isRefreshing, onRefresh) + val context = LocalContext.current + val memberAlreadyExistsError = stringResource(R.string.member_already_exists) + val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } + val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false) + + StatefulAlertDialog( + state = viewModel.dialogState, + onDismissRequest = { viewModel.dismissDialog() } + ) + + val projectOptionsProjectId = viewModel.showProjectOptionsDialogByProjectId + if (projectOptionsProjectId != null) { + val proj = remember(projectOptionsProjectId, viewModel.projects) { + viewModel.projects.find { it.id == projectOptionsProjectId } + } + Dialog( + onDismissRequest = { viewModel.showProjectOptionsDialogByProjectId = null }, + ) { + ProjectOptionsDialogContent( + onEditProject = { + onProjectAction(projectOptionsProjectId, 0) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onRemoveProject = { + onProjectAction(projectOptionsProjectId, 1) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onManageMembers = { + onProjectAction(projectOptionsProjectId, 2) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onManageCurrencies = { + onProjectAction(projectOptionsProjectId, 3) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onStatistics = { + onProjectAction(projectOptionsProjectId, 4) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onSettle = { + onProjectAction(projectOptionsProjectId, 5) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onShareProject = { + onProjectAction(projectOptionsProjectId, 6) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onExportProject = { + onProjectAction(projectOptionsProjectId, 7) + viewModel.showProjectOptionsDialogByProjectId = null + }, + onDismiss = { viewModel.showProjectOptionsDialogByProjectId = null }, + isArchived = proj?.isArchived == true, + accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN, + isShareable = proj?.isShareable() ?: true + ) + } + } + + val settlementProjectId = viewModel.showSettlementDialogByProjectId + if (settlementProjectId != null) { + val proj = remember(settlementProjectId, viewModel.projects) { + viewModel.projects.find { it.id == settlementProjectId } + } + if (proj != null) { + Dialog( + onDismissRequest = { viewModel.showSettlementDialogByProjectId = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + ProjectSettlementDialogContent( + proj = proj, + db = db, + onSettleBills = { transactions -> + BillsListUtils.createBillsFromTransactions( + db, + settlementProjectId, + transactions, + refreshCallback, + context + ) + viewModel.showSettlementDialogByProjectId = null + }, + onShare = { transactions, memberIdToName -> + BillsListUtils.shareSettlement(context, proj, transactions, memberIdToName) + }, + onDismiss = { viewModel.showSettlementDialogByProjectId = null } + ) + } + } + } + + val statisticsProjectId = viewModel.showStatisticsDialogByProjectId + if (statisticsProjectId != null) { + LaunchedEffect(statisticsProjectId) { + context.startActivity( + ProjectStatisticsActivity.createIntent( + context, + statisticsProjectId + ) + ) + viewModel.showStatisticsDialogByProjectId = null + } + } + + val manageMembersProjectId = viewModel.showMemberManagementDialogByProjectId + if (manageMembersProjectId != null) { + val members = viewModel.members // Use members from ViewModel as they are already loaded + Dialog( + onDismissRequest = { viewModel.showMemberManagementDialogByProjectId = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + MemberManagementDialogContent( + members = members, + onAddMember = { + viewModel.showAddMemberDialogByProjectId = manageMembersProjectId + viewModel.showMemberManagementDialogByProjectId = null + }, + onEditMember = { member -> + viewModel.showEditMemberDialogByProjectId = member.id + viewModel.showMemberManagementDialogByProjectId = null + }, + onDismiss = { viewModel.showMemberManagementDialogByProjectId = null } + ) + } + } + + val addMemberProjectId = viewModel.showAddMemberDialogByProjectId + if (addMemberProjectId != null) { + Dialog( + onDismissRequest = { viewModel.showAddMemberDialogByProjectId = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + MemberAddDialogContent( + onAdd = { memberName -> + val memberNames = + db.getMembersOfProject(addMemberProjectId, null).map { it.name } + if (memberNames.contains(memberName)) { + Toast.makeText( + context, + memberAlreadyExistsError, + Toast.LENGTH_SHORT + ).show() + } else { + val color = net.helcel.cowspent.android.helper.TextDrawable.getColorFromName(memberName) + db.addMemberAndSync( + DBMember( + 0, + 0, + addMemberProjectId, + memberName, + true, + 1.0, + DBBill.STATE_ADDED, + android.graphics.Color.red(color), + android.graphics.Color.green(color), + android.graphics.Color.blue(color), + null, + null + ) + ) + refreshCallback.refreshLists(false) + viewModel.showAddMemberDialogByProjectId = null + viewModel.showMemberManagementDialogByProjectId = addMemberProjectId + } + }, + onDismiss = { + viewModel.showAddMemberDialogByProjectId = null + viewModel.showMemberManagementDialogByProjectId = addMemberProjectId + } + ) + } + } + + val editMemberId = viewModel.showEditMemberDialogByProjectId + if (editMemberId != null) { + val memberToEdit = remember(editMemberId, viewModel.members) { + viewModel.members.find { it.id == editMemberId } + } + if (memberToEdit != null) { + Dialog( + onDismissRequest = { viewModel.showEditMemberDialogByProjectId = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + MemberEditDialogContent( + member = memberToEdit, + onSave = { name, weight, isActivated, r, g, b -> + db.updateMemberAndSync( + memberToEdit, + name, + weight, + isActivated, + r, + g, + b, + "", + "" + ) + refreshCallback.refreshLists(false) + viewModel.showEditMemberDialogByProjectId = null + viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId + }, + onDelete = { + db.deleteMember(editMemberId) + refreshCallback.refreshLists(false) + viewModel.showEditMemberDialogByProjectId = null + viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId + }, + onDismiss = { + viewModel.showEditMemberDialogByProjectId = null + viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId + } + ) + } + } + } + + val shareProjectId = viewModel.showShareDialogByProjectId + if (shareProjectId != null) { + val proj = remember(shareProjectId, viewModel.projects) { + viewModel.projects.find { it.id == shareProjectId } + } + if (proj != null) { + val shareIntentTitle = stringResource(R.string.share_share_intent_title, proj.name) + val shareChooserTitle = stringResource(R.string.share_share_chooser_title, proj.name) + Dialog( + onDismissRequest = { viewModel.showShareDialogByProjectId = null }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + ProjectShareDialogContent( + proj = proj, + onShare = { shareUrl -> + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, shareIntentTitle) + putExtra(Intent.EXTRA_TEXT, shareUrl) + } + val chooserIntent = Intent.createChooser(shareIntent, shareChooserTitle) + context.startActivity(chooserIntent) + }, + onDismiss = { viewModel.showShareDialogByProjectId = null } + ) + } + } + } + + Scaffold( + scaffoldState = scaffoldState, + drawerShape = RectangleShape, + topBar = { + TopAppBar( + title = { + if (isSearchExpanded) { + TextField( + value = viewModel.searchQuery, + onValueChange = { viewModel.searchQuery = it }, + placeholder = { Text(stringResource(R.string.action_search), color = MaterialTheme.colors.onPrimary.copy(alpha = 0.7f)) }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + cursorColor = MaterialTheme.colors.onPrimary, + textColor = MaterialTheme.colors.onPrimary, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + singleLine = true, + trailingIcon = { + IconButton(onClick = { + isSearchExpanded = false + viewModel.searchQuery = "" + }) { + Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.onPrimary) + } + } + ) + } else { + Column { + if (viewModel.title.isNotEmpty()) Text(viewModel.title) + else Text(stringResource(R.string.app_name)) + } + } + }, + navigationIcon = { + if (isSearchExpanded) { + IconButton(onClick = { + isSearchExpanded = false + viewModel.searchQuery = "" + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + } else { + IconButton(onClick = { + scope.launch { scaffoldState.drawerState.open() } + }) { + Icon(Icons.Default.Menu, contentDescription = null) + } + } + }, + actions = { + if (!isSearchExpanded) { + if (viewModel.hasUnlabeledBills) { + IconButton(onClick = onLabelBillsClick) { + Icon( + Icons.Default.Category, + contentDescription = stringResource(R.string.action_label_bills) + ) + } + } + IconButton(onClick = { isSearchExpanded = true }) { + Icon(Icons.Default.Search, contentDescription = null) + } + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 4.dp + ) + }, + floatingActionButton = { + val selectedProject = viewModel.projects.find { it.id == viewModel.selectedProjectId } + if (selectedProject != null && !selectedProject.isArchived) { + FloatingActionButton(onClick = onAddBillClick) { + Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_create_bill)) + } + } + }, + drawerContent = { + Drawer( + projects = viewModel.projects, + members = viewModel.members, + memberBalances = viewModel.memberBalances, + selectedProjectId = viewModel.selectedProjectId, + selectedMemberId = viewModel.selectedMemberId, + lastSyncText = viewModel.lastSyncText, + showArchived = showArchived, + onProjectClick = { + viewModel.selectedMemberId = null + onProjectClick(it) + scope.launch { scaffoldState.drawerState.close() } + }, + onProjectOptionsClick = { + onProjectOptionsClick(it) + }, + onMemberClick = { memberId -> + viewModel.selectedMemberId = memberId + refreshCallback.refreshLists(false) + scope.launch { scaffoldState.drawerState.close() } + }, + onAddProjectClick = { + onAddProjectClick() + scope.launch { scaffoldState.drawerState.close() } + }, + onAppSettingsClick = { + onAppSettingsClick() + scope.launch { scaffoldState.drawerState.close() } + } + ) + } + ) { padding -> + // Pull-to-refresh is not officially in Material 2 basic components, + // but we can use SwipeRefresh from accompanist or implement it manually. + // For simplicity and following common practices, I'll assume standard swipe refresh logic. + // Actually, there is androidx.compose.material.pullrefresh.pullRefresh in later Material 2. + + Box(modifier = Modifier + .padding(padding) + .fillMaxSize() + .pullRefresh(pullRefreshState)) { + when { + viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick) + viewModel.showNoMembers -> EmptyMembersState() + viewModel.showNoBills -> EmptyBillsState() + viewModel.bills.isEmpty() -> EmptyState() + else -> { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(viewModel.bills) { item -> + when (item) { + is DBBill -> { + val payer = viewModel.members.find { it.id == item.payerId } + BillItemRow(item, payer, onClick = { onBillClick(item) }) + } + is SectionItem -> SectionHeader(item.title) + } + } + } + } + } + PullRefreshIndicator( + refreshing = viewModel.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } +} + + + +@Preview(showBackground = true) +@Composable +fun BillItemRowPreview() { + MaterialTheme { + BillItemRow( + bill = DBBill(0, 0, 0, 1, 15.0, System.currentTimeMillis() / 1000, "Dinner", 0, "n", null, 0, "", 0).apply { + formattedWhat = "Dinner" + formattedSubtitle = "Alice \u2192 Group" + }, + payer = DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null), + onClick = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SectionHeaderPreview() { + MaterialTheme { + SectionHeader(title = "October 2023") + } +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview(showBackground = true) +@Composable +fun BillsListScreenPreview() { + MaterialTheme { + BillsListScreen( + viewModel = BillsListViewModel().apply { + title = "Demo Project" + members = listOf( + DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null) + ) + bills = listOf( + SectionItem("Today"), + DBBill(0, 0, 0, 1, 10.0, System.currentTimeMillis() / 1000, "Lunch", 0, "n", null, 0, "", 0).apply { + formattedWhat = "Lunch" + formattedSubtitle = "Alice \u2192 Group" + } + ) + }, + db = CowspentSQLiteOpenHelper.getInstance(LocalContext.current), + refreshCallback = object : IRefreshBillsListCallback { + override fun refreshLists(scrollToTop: Boolean) {} + }, + onAddBillClick = {}, + onBillClick = {}, + onProjectClick = {}, + onProjectOptionsClick = {}, + onProjectAction = { _, _ -> }, + onAccountSwitcherClick = {}, + onAddProjectClick = {}, + onAppSettingsClick = {}, + onLabelBillsClick = {}, + onRefresh = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyProjectsStatePreview() { + MaterialTheme { + EmptyProjectsState(onConfigureNextcloud = {}, onAddManually = {}) + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyMembersStatePreview() { + MaterialTheme { + EmptyMembersState() + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyBillsStatePreview() { + MaterialTheme { + EmptyBillsState() + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt new file mode 100644 index 0000000..b0eee62 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt @@ -0,0 +1,117 @@ +package net.helcel.cowspent.android.main + +import android.content.Context +import android.content.Intent +import net.helcel.cowspent.R +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.IRefreshBillsListCallback +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.round + +object BillsListUtils { + fun groupAndSectionBills( + bills: List, + membersMap: Map, + sdf: SimpleDateFormat, + context: Context + ): List { + val groupedBillsList = mutableListOf() + val groups = bills.groupBy { "${it.what}|${it.date}|${it.time}|${it.payerId}" } + val seenGroups = mutableSetOf() + + for (bill in bills) { + val groupKey = "${bill.what}|${bill.date}|${bill.time}|${bill.payerId}" + if (groupKey !in seenGroups) { + val group = groups[groupKey]!! + if (group.size == 1) { + groupedBillsList.add(bill) + } else { + val groupedBill = GroupedBill(group) + val payerName = membersMap[groupedBill.payerId]?.name ?: groupedBill.payerId.toString() + val allOwerIds = group.flatMap { it.billOwersIds }.distinct() + val owersNames = allOwerIds.joinToString(", ") { id -> + membersMap[id]?.name ?: id.toString() + } + groupedBill.formattedSubtitle = "$payerName \u2192 $owersNames" + groupedBillsList.add(groupedBill) + } + seenGroups.add(groupKey) + } + } + + val itemList: MutableList = ArrayList() + var lastDate = "" + val androidDateFormat = android.text.format.DateFormat.getDateFormat(context) + + for (bill in groupedBillsList) { + val billDate = bill.date + if (billDate != lastDate) { + val date = try { sdf.parse(billDate) } catch (_: Exception) { null } + val formattedDate = date?.let { androidDateFormat.format(it) } ?: billDate + itemList.add(SectionItem(formattedDate)) + lastDate = billDate + } + itemList.add(bill) + } + return itemList + } + + fun shareSettlement( + context: Context, + proj: DBProject, + transactions: List, + memberIdToName: Map + ) { + val projectName = proj.name.ifEmpty { proj.remoteId } + var text = context.getString(R.string.share_settle_intro, projectName) + "\n" + for (t in transactions) { + val amount = round(t.amount * 100.0) / 100.0 + text += "\n" + context.getString( + R.string.share_settle_sentence, + memberIdToName[t.owerMemberId], + memberIdToName[t.receiverMemberId], + amount + ) + } + val shareIntent = Intent() + shareIntent.action = Intent.ACTION_SEND + shareIntent.type = "text/plain" + shareIntent.putExtra( + Intent.EXTRA_SUBJECT, + context.getString(R.string.share_settle_title, projectName) + ) + shareIntent.putExtra(Intent.EXTRA_TEXT, text) + val chooserIntent = Intent.createChooser( + shareIntent, + context.getString(R.string.share_settle_title, projectName) + ) + context.startActivity(chooserIntent) + } + + fun createBillsFromTransactions( + db: CowspentSQLiteOpenHelper, + projectId: Long, + transactions: List, + refreshCallback: IRefreshBillsListCallback, + context: Context + ) { + val timestamp = System.currentTimeMillis() / 1000 + for (t in transactions) { + val owerId = t.owerMemberId + val receiverId = t.receiverMemberId + val amount = t.amount + val bill = DBBill( + 0, 0, projectId, owerId, amount, + timestamp, context.getString(R.string.settle_bill_what), + DBBill.STATE_ADDED, DBBill.NON_REPEATED, + DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, + "", DBBill.PAYMODE_ID_NONE + ) + bill.billOwers += DBBillOwer(0, 0, receiverId) + db.addBill(bill) + } + refreshCallback.refreshLists(true) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt new file mode 100644 index 0000000..06df8f1 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -0,0 +1,827 @@ +package net.helcel.cowspent.android.main + +import android.Manifest +import android.app.SearchManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Base64 +import android.util.Log +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircleOutline +import androidx.compose.material.icons.filled.Sync +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import com.nextcloud.android.sso.helper.SingleAccountHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.account.AccountActivity +import net.helcel.cowspent.android.bill_edit.EditBillActivity +import net.helcel.cowspent.android.bill_label.LabelBillsActivity +import net.helcel.cowspent.android.currencies.ManageCurrenciesActivity +import net.helcel.cowspent.android.helper.showToast +import net.helcel.cowspent.android.project.create.NewProjectActivity +import net.helcel.cowspent.android.project.edit.EditProjectActivity +import net.helcel.cowspent.android.settings.PreferencesActivity +import net.helcel.cowspent.model.Category +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.GroupedBill +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.persistence.CowspentServerSyncHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.BillFormatter +import net.helcel.cowspent.util.CospendClientUtil +import net.helcel.cowspent.util.ExportUtil +import net.helcel.cowspent.util.ICallback +import net.helcel.cowspent.util.IRefreshBillsListCallback +import net.helcel.cowspent.util.SupportUtil +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class BillsListViewActivity : + AppCompatActivity(), + IRefreshBillsListCallback { + + private val viewModel: BillsListViewModel by viewModels() + + companion object { + var DEBUG = false + + private val TAG = BillsListViewActivity::class.java.simpleName + + private const val SAVED_STATE_NAVIGATION_SELECTION = "navigationSelection" + private const val SAVED_STATE_NAVIGATION_OPEN = "navigationOpen" + + private var contentToExport = "" + var isActivityVisible = false + private set + } + + private var navigationSelection = Category(null, null) + private var navigationOpen: String? = "" + + private var mActionMode: ActionMode? = null + private lateinit var db: CowspentSQLiteOpenHelper + + private val syncCallBack = object : ICallback { + override fun onFinish() { + mActionMode?.finish() + refreshLists() + viewModel.isRefreshing = false + } + + override fun onFinish(result: String, message: String) {} + + override fun onScheduled() { + viewModel.isRefreshing = false + } + } + + private val addProjectLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + var pid = data.getLongExtra(MainConstants.CREATED_PROJECT, 0) + var created = true + if (pid == 0L) { + created = false + pid = data.getLongExtra(MainConstants.ADDED_PROJECT, 0) + } + if (DEBUG) Log.d(TAG, "BILLS request code : addproject $pid") + if (pid != 0L) { + viewModel.selectedMemberId = null + setSelectedProject(pid) + Log.d(TAG, "CREATED project id: $pid") + lifecycleScope.launch { + val addedProj = withContext(Dispatchers.IO) { db.getProject(pid) } + val message: String + val title: String + if (created) { + Log.e(TAG, "CREATED !!!") + title = getString(R.string.project_create_success_title) + message = getString(R.string.project_create_success_message, addedProj?.remoteId) + } else { + Log.e(TAG, "ADDED !!!") + title = getString(R.string.project_add_success_title) + message = getString(R.string.project_add_success_message, addedProj?.remoteId) + } + showDialog(message, title, Icons.Default.AddCircleOutline) + } + } + } + setupDrawerProjects() + } + + private val serverSettingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + updateUsernameInDrawer() + db = CowspentSQLiteOpenHelper.getInstance(this) + if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) { + db.cowspentServerSyncHelper.runAccountProjectsSync() + } + if (!db.cowspentServerSyncHelper.isSyncPossible) { + if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) { + Toast.makeText(applicationContext, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show() + } + } + } + } + + private val createBillLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + } + + private val editBillLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + val billId = data.getLongExtra(MainConstants.BILL_TO_DUPLICATE, 0) + if (billId != 0L) { + duplicateBill(billId) + } + } + } + + private val editProjectLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + var pid = data.getLongExtra(MainConstants.DELETED_PROJECT, 0) + if (pid != 0L) { + setSelectedProject(0) + } + pid = data.getLongExtra(MainConstants.EDITED_PROJECT, 0) + if (pid != 0L) { + viewModel.selectedMemberId = null + setSelectedProject(pid) + } + } + setupDrawerProjects() + } + + private val labelBillsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + refreshLists() + } + + private val saveFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + val fileUri = data.data + fileUri?.let { saveToFileUri(contentToExport, it) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isActivityVisible = true + if (savedInstanceState != null) { + navigationSelection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + savedInstanceState.getSerializable(SAVED_STATE_NAVIGATION_SELECTION, Category::class.java)!! + } else { + @Suppress("DEPRECATION") + savedInstanceState.getSerializable(SAVED_STATE_NAVIGATION_SELECTION) as Category + } + navigationOpen = savedInstanceState.getString(SAVED_STATE_NAVIGATION_OPEN) + } + + db = CowspentSQLiteOpenHelper.getInstance(this) + + setupDrawerProjects() + + updateUsernameInDrawer() + + setContent { + ThemeUtils.CowspentTheme { + BillsListScreen( + viewModel = viewModel, + db = db, + refreshCallback = this, + onAddBillClick = { + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + if (selectedProjectId != 0L) { + lifecycleScope.launch { + val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) } + if (members.isEmpty()) { + showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member)) + } else { + val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) } + val createIntent = Intent(applicationContext, EditBillActivity::class.java).apply { + putExtra(EditBillActivity.PARAM_PROJECT_ID, selectedProjectId) + putExtra(EditBillActivity.PARAM_PROJECT_TYPE, proj?.type?.id) + } + createBillLauncher.launch(createIntent) + } + } + } + }, + onBillClick = { bill: DBBill -> + lifecycleScope.launch { + val pid = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + val proj = withContext(Dispatchers.IO) { db.getProject(pid) } + val intent = Intent(applicationContext, EditBillActivity::class.java).apply { + if (bill is GroupedBill) { + val ids = bill.sourceBills.map { it.id }.toLongArray() + putExtra(EditBillActivity.PARAM_GROUPED_BILL_IDS, ids) + } else { + putExtra(EditBillActivity.PARAM_BILL_ID, bill.id) + } + putExtra(EditBillActivity.PARAM_PROJECT_TYPE, proj?.type?.id) + putExtra(EditBillActivity.PARAM_PROJECT_ID, pid) + } + editBillLauncher.launch(intent) + } + }, + onProjectClick = { pid: Long -> onProjectClick(pid) }, + onProjectOptionsClick = { pid: Long -> onManageProjectClick(pid) }, + onProjectAction = { pid, actionIndex -> + when (actionIndex) { + 0 -> onEditProjectClick(pid) + 1 -> onRemoveProjectClick(pid) + 2 -> onManageMembersClick(pid) + 3 -> onManageCurrenciesClick(pid) + 4 -> onProjectStatisticsClick(pid) + 5 -> onSettleProjectClick(pid) + 6 -> onShareProjectClick(pid) + 7 -> onExportProjectClick(pid) + } + }, + onAccountSwitcherClick = { + serverSettingsLauncher.launch(Intent(this, AccountActivity::class.java)) + }, + onAddProjectClick = { addProject() }, + onAppSettingsClick = { + serverSettingsLauncher.launch(Intent(this, PreferencesActivity::class.java)) + }, + onLabelBillsClick = { + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + if (selectedProjectId != 0L) { + labelBillsLauncher.launch(LabelBillsActivity.createIntent(this, selectedProjectId)) + } + }, + onRefresh = { synchronize(true) } + ) + } + } + + lifecycleScope.launch { + val empty = withContext(Dispatchers.IO) { db.projects.isEmpty() } + if (empty && !CowspentServerSyncHelper.isNextcloudAccountConfigured(this@BillsListViewActivity)) { + viewModel.showNoProjects = true + } + + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val selectedProjectId = preferences.getLong("selected_project", 0) + if (selectedProjectId == 0L) { + val dbProjects = withContext(Dispatchers.IO) { db.projects } + if (dbProjects.isNotEmpty()) { + setSelectedProject(dbProjects[0].id) + } + } + } + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + } + }) + + val projectToSelect = intent.getLongExtra(MainConstants.PARAM_PROJECT_TO_SELECT, 0) + if (projectToSelect != 0L) { + setSelectedProject(projectToSelect) + lifecycleScope.launch { + val project = withContext(Dispatchers.IO) { db.getProject(projectToSelect) } + val dialogContent = intent.getStringExtra(MainConstants.PARAM_DIALOG_CONTENT) + if (dialogContent != null && project != null) { + viewModel.showDialog( + title = getString(R.string.activity_dialog_title, project.name), + message = dialogContent, + positiveText = getString(android.R.string.ok), + icon = Icons.Default.Sync + ) + } + } + } + } + + override fun onResume() { + super.onResume() + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val selectedProjectId = preferences.getLong("selected_project", 0) + if (selectedProjectId != 0L) { + refreshLists() + } + viewModel.isRefreshing = false + + if (db.cowspentServerSyncHelper.isSyncPossible) { + db.cowspentServerSyncHelper.addCallbackPull(syncCallBack) + synchronize() + } + + registerBroadcastReceiver() + updateAvatarInDrawer(CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) + isActivityVisible = true + } + + override fun onPause() { + super.onPause() + try { + unregisterReceiver(mBroadcastReceiver) + } catch (_: RuntimeException) { + if (DEBUG) Log.d(TAG, "RECEIVER PROBLEM, let's ignore it...") + } + isActivityVisible = false + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(SAVED_STATE_NAVIGATION_SELECTION, navigationSelection) + outState.putString(SAVED_STATE_NAVIGATION_OPEN, navigationOpen) + } + + private fun setupDrawerProjects() { + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + lifecycleScope.launch { + val projects = withContext(Dispatchers.IO) { db.projects } + viewModel.projects = projects + setSelectedProject(selectedProjectId) + } + } + + fun onProjectClick(projectId: Long) { + if (viewModel.selectedProjectId != projectId) { + viewModel.selectedMemberId = null + } + setSelectedProject(projectId) + navigationSelection = Category(null, null) + refreshLists(true) + + synchronize() + } + + fun onManageProjectClick(projectId: Long) { + viewModel.showProjectOptionsDialogByProjectId = projectId + } + + private fun onEditProjectClick(projectId: Long) { + if (projectId == 0L) return + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } + if (proj?.isLocal == false) { + val intent = Intent(applicationContext, EditProjectActivity::class.java).apply { + putExtra(EditProjectActivity.PARAM_PROJECT_ID, projectId) + } + editProjectLauncher.launch(intent) + } else { + showToast(this@BillsListViewActivity, getString(R.string.edit_project_local_impossible)) + } + } + } + + private fun onRemoveProjectClick(projectId: Long) { + if (projectId == 0L) return + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch + + viewModel.showDialog( + title = getString(R.string.confirm_remove_project_dialog_title), + message = if (!proj.isLocal) getString(R.string.confirm_remove_project_dialog_message) else null, + positiveText = getString(R.string.simple_yes), + onConfirm = { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db.deleteProject(projectId) + val dbProjects = db.projects + if (dbProjects.isNotEmpty()) setSelectedProject(dbProjects[0].id) else setSelectedProject(0) + } + setupDrawerProjects() + refreshLists() + synchronize() + val projectNameString = proj.name.ifEmpty { proj.remoteId } + showToast(this@BillsListViewActivity, getString(R.string.remove_project_confirmation, projectNameString)) + } + }, + negativeText = getString(R.string.simple_no) + ) + } + } + + fun onManageMembersClick(projectId: Long) { + if (projectId == 0L) return + lifecycleScope.launch { + val project = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch + if (project.myAccessLevel != DBProject.ACCESS_LEVEL_UNKNOWN && project.myAccessLevel < DBProject.ACCESS_LEVEL_MAINTAINER) { + showToast(this@BillsListViewActivity, getString(R.string.insufficient_access_level)) + return@launch + } + + viewModel.showMemberManagementDialogByProjectId = projectId + } + } + + fun onManageCurrenciesClick(projectId: Long) { + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } + if (proj != null && proj.type == ProjectType.COSPEND) { + startActivity(Intent(applicationContext, ManageCurrenciesActivity::class.java).apply { + putExtra(ManageCurrenciesActivity.EXTRA_PROJECT_ID, projectId) + }) + } else showToast(this@BillsListViewActivity, getString(R.string.currency_management_unavailable)) + } + } + + fun onProjectStatisticsClick(projectId: Long) { + viewModel.showStatisticsDialogByProjectId = projectId + } + + fun onSettleProjectClick(projectId: Long) { + viewModel.showSettlementDialogByProjectId = projectId + } + + fun onShareProjectClick(projectId: Long) { + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } + if (projectId != 0L && proj?.isShareable() == true) viewModel.showShareDialogByProjectId = projectId + else showToast(this@BillsListViewActivity, getString(R.string.share_impossible), Toast.LENGTH_LONG) + } + } + + fun onExportProjectClick(projectId: Long) { + if (projectId == 0L) return + lifecycleScope.launch { + contentToExport = withContext(Dispatchers.IO) { ExportUtil.createExportContent(db, projectId) } + val fileName = withContext(Dispatchers.IO) { ExportUtil.createExportFileName(db, projectId) } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/csv" + putExtra(Intent.EXTRA_TITLE, fileName) + } + saveFileLauncher.launch(intent) + } + } + + private fun saveToFileUri(content: String, fileUri: Uri) { + try { + contentResolver.openOutputStream(fileUri)?.use { outputStream -> + outputStream.writer().use { it.write(content) } + } + showToast(this,getString(R.string.file_saved_success, fileUri.lastPathSegment?.replace(Environment.getExternalStorageDirectory().toString(), ""))) + } catch (e: IOException) { + Log.e("Exception", "File write failed: $e") + showToast(this,e.toString()) + } + } + + private fun addProject() { + lifecycleScope.launch { + var defaultNcUrl = if (CowspentServerSyncHelper.isNextcloudAccountConfigured(this@BillsListViewActivity)) { + CowspentServerSyncHelper.getNextcloudAccountServerUrl(this@BillsListViewActivity) + } else "https://mynextcloud.org" + + val dbProjects = withContext(Dispatchers.IO) { db.projects } + for (project in dbProjects) { + val url = project.serverUrl + if (!url.isNullOrEmpty() && url.contains("/index.php/apps/cospend")) { + defaultNcUrl = url.replace("/index.php/apps/cospend", "") + break + } + } + val intent = Intent(applicationContext, NewProjectActivity::class.java).apply { + putExtra(NewProjectActivity.PARAM_DEFAULT_NC_URL, defaultNcUrl) + } + addProjectLauncher.launch(intent) + } + } + + private fun setSelectedProject(projectId: Long) { + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + preferences.edit { putLong("selected_project", projectId) } + + lifecycleScope.launch { + val (proj, members, memberBalances) = withContext(Dispatchers.IO) { + var proj = db.getProject(projectId) + if (proj == null) { + val dbProjects = db.projects + if (dbProjects.isNotEmpty()) { + proj = dbProjects[0] + preferences.edit { putLong("selected_project", proj.id) } + } else { + return@withContext Triple(null, emptyList(), emptyMap()) + } + } + + val members = db.getMembersOfProject(proj.id, null) + val bills = db.getBillsOfProject(proj.id) + val balances = HashMap() + SupportUtil.getStats( + members, bills, + mutableMapOf(), balances, mutableMapOf(), mutableMapOf(), + -1000, -1000, null, null + ) + Triple(proj, members, balances) + } + + if (proj == null) { + viewModel.selectedProjectId = 0L + return@launch + } + + if (viewModel.selectedProjectId != proj.id) { + viewModel.selectedMemberId = null + } + viewModel.selectedProjectId = proj.id + viewModel.members = members + viewModel.memberBalances = memberBalances + + updateLastSyncText(proj) + } + } + + private fun updateLastSyncText(proj: DBProject?) { + if (proj == null || proj.isLocal) { + viewModel.lastSyncText = "" + } else { + val lastSyncTimestamp = proj.lastSyncedTimestamp ?: 0 + val cal = Calendar.getInstance().apply { timeInMillis = lastSyncTimestamp * 1000 } + val text = getString(R.string.drawer_last_sync_text, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)) + viewModel.lastSyncText = text + } + } + + fun refreshLists() = refreshLists(false) + + override fun refreshLists(scrollToTop: Boolean) { + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + + lifecycleScope.launch { + val (projId, projName) = withContext(Dispatchers.IO) { + if (selectedProjectId != 0L) { + db.getProject(selectedProjectId)?.let { + it.id to (if (it.name == "null" || it.name.isEmpty()) it.remoteId else it.name) + } ?: (0L to "") + } else { + 0L to "" + } + } + + val title = if (selectedProjectId != 0L) projName else getString(R.string.app_name) + + setSelectedProject(selectedProjectId) + viewModel.title = title + val query = viewModel.searchQuery.ifEmpty { null } + + val (ljItems, memberCount) = withContext(Dispatchers.IO) { + val db = CowspentSQLiteOpenHelper.getInstance(applicationContext) + val billList: List = if (projId != 0L) { + db.searchBills(query, projId) + } else { + ArrayList() + } + + val bills = billList.filter { + val mid = viewModel.selectedMemberId + mid == null || mid == it.payerId || it.billOwersIds.contains(mid) + } + + viewModel.hasUnlabeledBills = bills.any { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED } + + val projectMembers = db.getMembersOfProject(projId, null) + val memberMap = projectMembers.associateBy { it.id } + + val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.remoteId } + val projectCategories = db.getCategories(projId).associateBy { it.remoteId } + + BillFormatter.formatBills( + bills, + memberMap, + projectCategories, + projectPaymentModes + ) + + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) + val itemList = BillsListUtils.groupAndSectionBills( + bills, + memberMap, + sdf, + applicationContext + ) + + itemList to projectMembers.size + } + + viewModel.showNoProjects = false + viewModel.showNoMembers = false + viewModel.showNoBills = false + + when { + memberCount == 0 -> { + viewModel.showNoMembers = true + } + ljItems.isEmpty() -> { + viewModel.showNoBills = true + viewModel.bills = emptyList() + } + else -> { + viewModel.bills = ljItems + } + } + } + } + + override fun onNewIntent(intent: Intent) { + if (Intent.ACTION_SEARCH == intent.action) { + viewModel.searchQuery = intent.getStringExtra(SearchManager.QUERY) ?: "" + } + super.onNewIntent(intent) + } + + private fun duplicateBill(billId: Long) { + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + if (selectedProjectId != 0L) { + lifecycleScope.launch { + val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) } + if (members.isEmpty()) { + showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member)) + } else { + val projType = withContext(Dispatchers.IO) { db.getProject(selectedProjectId)?.type?.id } + val intent = Intent(applicationContext, EditBillActivity::class.java).apply { + putExtra(EditBillActivity.PARAM_PROJECT_ID, selectedProjectId) + putExtra(EditBillActivity.PARAM_PROJECT_TYPE, projType) + putExtra(EditBillActivity.PARAM_BILL_ID_TO_DUPLICATE, billId) + } + createBillLauncher.launch(intent) + } + } + } + } + + private fun showDialog(msg: String, title: String, icon: ImageVector) { + viewModel.showDialog( + title = title, + message = msg, + positiveText = getString(android.R.string.ok), + icon = icon + ) + } + + private fun updateUsernameInDrawer() { + if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) { + val text = getString(R.string.drawer_no_account) + viewModel.accountName = text + updateAvatarInDrawer(false) + } else { + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + val (user, server) = if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(this) + ssoAccount.userId to ssoAccount.url.replace(Regex("/+$"), "").replace(Regex("^https?://"), "") + } catch (_: Exception) { "error" to "error" } + } else { + preferences.getString(AccountActivity.SETTINGS_USERNAME, "") to preferences.getString( + AccountActivity.SETTINGS_URL, "")?.replace(Regex("/+$"), "")?.replace(Regex("^https?://"), "") + } + val text = "$user@$server" + viewModel.accountName = text + updateAvatarInDrawer(true) + } + } + + private fun updateAvatarInDrawer(isConfigured: Boolean) { + if (isConfigured) { + val avatarB64 = PreferenceManager.getDefaultSharedPreferences(this).getString(getString(R.string.pref_key_avatar), "") + if (!avatarB64.isNullOrEmpty()) { + try { + val bytes = Base64.decode(avatarB64, Base64.DEFAULT) + viewModel.userAvatar = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + } catch (_: Exception) { + viewModel.userAvatar = null + } + } else { + viewModel.userAvatar = null + } + } else { + viewModel.userAvatar = null + } + } + + private fun synchronize(manual: Boolean = false) { + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val offlineMode = preferences.getBoolean(getString(R.string.pref_key_offline_mode), false) + if (offlineMode && !manual) { + return + } + + if (db.cowspentServerSyncHelper.isSyncPossible) { + viewModel.isRefreshing = true + val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0) + if (selectedProjectId != 0L) { + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) } + if (proj != null && !proj.isLocal) { + db.cowspentServerSyncHelper.addCallbackPull(syncCallBack) + db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId) + } else viewModel.isRefreshing = false + } + } else viewModel.isRefreshing = false + if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) { + db.cowspentServerSyncHelper.runAccountProjectsSync() + } + } + } + + private fun registerBroadcastReceiver() { + val filter = IntentFilter().apply { + addAction(MainConstants.BROADCAST_PROJECT_SYNC_FAILED) + addAction(MainConstants.BROADCAST_PROJECT_SYNCED) + addAction(MainConstants.BROADCAST_SYNC_PROJECT) + addAction(MainConstants.BROADCAST_NETWORK_AVAILABLE) + addAction(MainConstants.BROADCAST_NETWORK_UNAVAILABLE) + addAction(MainConstants.BROADCAST_AVATAR_UPDATED) + addAction(MainConstants.BROADCAST_SSO_TOKEN_MISMATCH) + addAction(MainConstants.BROADCAST_ACCOUNT_PROJECTS_SYNC_FAILED) + addAction(MainConstants.BROADCAST_ACCOUNT_PROJECTS_SYNCED) + } + ContextCompat.registerReceiver(this, mBroadcastReceiver, filter, ContextCompat.RECEIVER_EXPORTED) + } + + private val mBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + if (intent?.action == null) return + when (intent.action) { + MainConstants.BROADCAST_PROJECT_SYNC_FAILED -> { + val errorMessage = intent.getStringExtra(MainConstants.BROADCAST_ERROR_MESSAGE) + val projectId = intent.getLongExtra(MainConstants.BROADCAST_PROJECT_ID, 0) + if (projectId != 0L) { + lifecycleScope.launch { + val project = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch + viewModel.showDialog( + title = getString(R.string.sync_error_dialog_title), + message = getString(R.string.sync_error_dialog_full_content, project.name, errorMessage), + positiveText = getString(R.string.simple_close), + icon = Icons.Default.Sync + ) + } + } + } + MainConstants.BROADCAST_PROJECT_SYNCED -> { + setupDrawerProjects() + refreshLists() + } + MainConstants.BROADCAST_SYNC_PROJECT -> { + synchronize() + } + MainConstants.BROADCAST_NETWORK_AVAILABLE -> { + } + MainConstants.BROADCAST_NETWORK_UNAVAILABLE -> { + } + MainConstants.BROADCAST_SSO_TOKEN_MISMATCH -> { + viewModel.showDialog( + title = getString(R.string.sync_error_dialog_title), + message = getString(R.string.error_token_mismatch), + positiveText = getString(R.string.simple_close), + icon = Icons.Default.Sync + ) + } + MainConstants.BROADCAST_AVATAR_UPDATED -> { + val memberId = intent.getLongExtra(MainConstants.BROADCAST_AVATAR_UPDATED_MEMBER, 0) + if (memberId == 0L) updateAvatarInDrawer(true) else refreshLists() + } + MainConstants.BROADCAST_ACCOUNT_PROJECTS_SYNCED -> { + setupDrawerProjects() + val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) + lifecycleScope.launch { + val dbProjects = withContext(Dispatchers.IO) { db.projects } + if (prefs.getLong("selected_project", 0) == 0L && dbProjects.isNotEmpty()) { + setSelectedProject(dbProjects[0].id) + refreshLists() + if (db.cowspentServerSyncHelper.isSyncPossible) { + db.cowspentServerSyncHelper.addCallbackPull(syncCallBack) + synchronize() + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewModel.kt new file mode 100644 index 0000000..caafc3f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewModel.kt @@ -0,0 +1,77 @@ +package net.helcel.cowspent.android.main + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.Item +import net.helcel.cowspent.android.helper.DialogState + +import androidx.compose.ui.graphics.vector.ImageVector + +class BillsListViewModel : ViewModel() { + var projects by mutableStateOf>(emptyList()) + var members by mutableStateOf>(emptyList()) + var memberBalances by mutableStateOf>(emptyMap()) + var selectedProjectId by mutableLongStateOf(0L) + var selectedMemberId by mutableStateOf(null) + var bills by mutableStateOf>(emptyList()) + var isRefreshing by mutableStateOf(false) + var searchQuery by mutableStateOf("") + var title by mutableStateOf("") + var accountName by mutableStateOf("") + var userAvatar by mutableStateOf(null) + var lastSyncText by mutableStateOf("") + + var showNoProjects by mutableStateOf(false) + var showNoMembers by mutableStateOf(false) + var showNoBills by mutableStateOf(false) + var hasUnlabeledBills by mutableStateOf(false) + + var dialogState by mutableStateOf(null) + + var showProjectOptionsDialogByProjectId by mutableStateOf(null) + var showSettlementDialogByProjectId by mutableStateOf(null) + var showStatisticsDialogByProjectId by mutableStateOf(null) + var showMemberManagementDialogByProjectId by mutableStateOf(null) + var showAddMemberDialogByProjectId by mutableStateOf(null) + var showEditMemberDialogByProjectId by mutableStateOf(null) + var showShareDialogByProjectId by mutableStateOf(null) + + fun showDialog( + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: List? = null, + itemIcons: List? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null + ) { + dialogState = DialogState( + title = title, + message = message, + icon = icon, + items = items, + itemIcons = itemIcons, + positiveText = positiveText, + negativeText = negativeText, + neutralText = neutralText, + onConfirm = onConfirm, + onCancel = onCancel, + onNeutral = onNeutral, + onItemSelected = onItemSelected + ) + } + + fun dismissDialog() { + dialogState = null + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/MainConstants.kt b/app/src/main/java/net/helcel/cowspent/android/main/MainConstants.kt new file mode 100644 index 0000000..5d5b221 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/main/MainConstants.kt @@ -0,0 +1,36 @@ +package net.helcel.cowspent.android.main + +@Suppress("unused") +object MainConstants { + const val BROADCAST_EXTRA_PARAM = "net.helcel.cowspent.broadcast_extra_param" + const val BROADCAST_ERROR_MESSAGE = "net.helcel.cowspent.broadcast_error_message" + const val BROADCAST_PROJECT_ID = "net.helcel.cowspent.broadcast_project_id" + const val BROADCAST_ACCOUNT_PROJECTS_SYNC_FAILED = "net.helcel.cowspent.broadcast_acc_proj_failed" + const val BROADCAST_SSO_TOKEN_MISMATCH = "net.helcel.cowspent.broadcast.token_mismatch" + const val BROADCAST_ACCOUNT_PROJECTS_SYNCED = "net.helcel.cowspent.broadcast.broadcast_acc_proj_synced" + + const val BROADCAST_PROJECT_SYNC_FAILED = "net.helcel.cowspent.broadcast.project_sync_failed" + const val BROADCAST_PROJECT_SYNCED = "net.helcel.cowspent.broadcast.project_synced" + const val BROADCAST_SYNC_PROJECT = "net.helcel.cowspent.broadcast.sync_project" + const val BROADCAST_NETWORK_AVAILABLE = "net.helcel.cowspent.broadcast.network_available" + const val BROADCAST_NETWORK_UNAVAILABLE = "net.helcel.cowspent.broadcast.network_unavailable" + const val BROADCAST_AVATAR_UPDATED = "net.helcel.cowspent.broadcast.avatar_updated" + const val BROADCAST_AVATAR_UPDATED_MEMBER = "net.helcel.cowspent.broadcast.avatar_updated_for_member" + + const val MAIN_CHANNEL_ID = 1234567890 + + const val PARAM_DIALOG_CONTENT = "net.helcel.cowspent.PARAM_DIALOG_CONTENT" + const val PARAM_PROJECT_TO_SELECT = "net.helcel.cowspent.PARAM_PROJECT_TO_SELECT" + + const val SAVED_BILL_ID = "net.helcel.cowspent.saved_bill_id" + const val CREATED_PROJECT = "net.helcel.cowspent.created_project" + const val ADDED_PROJECT = "net.helcel.cowspent.added_project" + const val EDITED_PROJECT = "net.helcel.cowspent.edited_project" + const val DELETED_PROJECT = "net.helcel.cowspent.deleted_project" + const val DELETED_BILL = "net.helcel.cowspent.deleted_bill" + const val BILL_TO_DUPLICATE = "net.helcel.cowspent.bill_to_duplicate" + + const val KEY_QR_CODE = "net.helcel.cowspent.android.helper.key_qr_code" + + const val CREDENTIALS_CHANGED = "net.helcel.cowspent.CREDENTIALS_CHANGED" +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt new file mode 100644 index 0000000..9ef41cc --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt @@ -0,0 +1,175 @@ +package net.helcel.cowspent.android.project + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import android.util.Log +import com.opencsv.CSVReader +import net.helcel.cowspent.R +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import java.io.InputStreamReader +import java.text.SimpleDateFormat +import java.util.* + +object ProjectImportHelper { + + @SuppressLint("Range") + fun getFileName(contentResolver: ContentResolver, uri: Uri): String { + var result: String? = null + if (uri.scheme == "content") { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } + } + if (result == null) { + result = uri.path?.let { p -> + val cut = p.lastIndexOf('/') + if (cut != -1) p.substring(cut + 1) else p + } + } + return result ?: "project.csv" + } + + fun importFromFile( + context: Context, + db: CowspentSQLiteOpenHelper, + fileUri: Uri, + onSuccess: (Long) -> Unit, + onError: (String) -> Unit + ) { + val contentResolver = context.contentResolver + try { + val projectRemoteId = getFileName(contentResolver, fileUri).replace("\\.csv$".toRegex(), "") + val inputStream = contentResolver.openInputStream(fileUri) ?: return + val reader = CSVReader(InputStreamReader(inputStream)) + + var previousLineEmpty = false + var currentSection: String? = null + var row = 0 + var mainCurrencyName: String? = null + val columns = mutableMapOf() + val paymentModes = mutableListOf() + val categories = mutableListOf() + val currencies = mutableListOf() + val bills = mutableListOf() + val membersActive = mutableMapOf() + val membersWeight = mutableMapOf() + val billRemoteIdToPayerName = mutableMapOf() + val billRemoteIdToOwerStr = mutableMapOf() + + var nextLine: Array? + while (reader.readNext().also { nextLine = it } != null) { + val line = nextLine!! + val allFieldsEmpty = line.all { it.isEmpty() } + + if (allFieldsEmpty) { + previousLineEmpty = true + } else if (row == 0 || previousLineEmpty) { + previousLineEmpty = false + columns.clear() + line.forEachIndexed { index, s -> columns[s] = index } + + currentSection = when { + columns.containsKey("what") && columns.containsKey("amount") -> "bills" + columns.containsKey("categoryid") && columns.containsKey("categoryname") -> "categories" + columns.containsKey("exchange_rate") && columns.containsKey("currencyname") -> "currencies" + else -> { + onError(context.getString(R.string.import_error_header, row)) + return + } + } + } else { + previousLineEmpty = false + when (currentSection) { + "categories" -> { + categories.add(DBCategory(0, line[columns["categoryid"]!!].toLong(), 0, line[columns["categoryname"]!!], line[columns["icon"]!!], line[columns["color"]!!])) + } + "paymentmodes" -> { + paymentModes.add(DBPaymentMode(0, line[columns["categoryid"]!!].toLong(), 0, line[columns["categoryname"]!!], line[columns["icon"]!!], line[columns["color"]!!])) + } + "currencies" -> { + val name = line[columns["currencyname"]!!] + val rate = line[columns["exchange_rate"]!!].toDouble() + if (rate == 1.0) mainCurrencyName = name + currencies.add(DBCurrency(0, 0, 0, name, rate, DBBill.STATE_OK)) + } + "bills" -> { + val what = if (columns.containsKey("what")) line[columns["what"]!!] else "" + val comment = if (columns.containsKey("comment")) line[columns["comment"]!!] else "" + val amount = if (columns.containsKey("amount")) line[columns["amount"]!!].toDouble() else 0.0 + val timestamp: Long = when { + columns.containsKey("timestamp") -> line[columns["timestamp"]!!].toLong() + columns.containsKey("date") -> { + try { + SimpleDateFormat("yyyy-MM-dd", Locale.ROOT).parse(line[columns["date"]!!])!!.time / 1000 + } catch (_: Exception) { + onError(context.getString(R.string.import_error_date, row)) + return + } + } + else -> 0 + } + val payerName = if (columns.containsKey("payer_name")) line[columns["payer_name"]!!] else "" + val payerWeight = if (columns.containsKey("payer_weight")) line[columns["payer_weight"]!!].toDouble() else 1.0 + val owersStr = if (columns.containsKey("owers")) line[columns["owers"]!!] else "" + val payerActive = columns.containsKey("payer_active") && line[columns["payer_active"]!!] == "1" + val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toInt() else 0 + val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toInt() else 0 + val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null + + membersActive[payerName] = payerActive + membersWeight[payerName] = payerWeight + + if (owersStr.trim().isEmpty()) { + onError(context.getString(R.string.import_error_owers, row)) + return + } + + if (what != "deleteMeIfYouWant") { + billRemoteIdToOwerStr[row.toLong()] = owersStr + val owersArray = owersStr.split(", ").filter { it.isNotEmpty() } + for (ower in owersArray) { + if (!membersWeight.containsKey(ower.trim())) { + membersWeight[ower.trim()] = 1.0 + } + } + bills.add(DBBill(0, row.toLong(), 0, 0, amount, timestamp, what, DBBill.STATE_OK, "n", pm, catId, comment, pmId)) + billRemoteIdToPayerName[row.toLong()] = payerName + } + } + } + } + row++ + } + + val memberNameToId = mutableMapOf() + val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null)) + + paymentModes.forEach { db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color)) } + categories.forEach { db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color)) } + currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) } + + membersWeight.keys.forEach { mName -> + memberNameToId[mName] = db.addMember(DBMember(0, 0, pid, mName, membersActive[mName] ?: true, membersWeight[mName] ?: 1.0, DBBill.STATE_OK, null, null, null, null, null)) + } + + bills.forEach { b -> + val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L + val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, b.categoryRemoteId, b.comment, b.paymentModeRemoteId)) + billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower -> + memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) } + } + } + onSuccess(pid) + + } catch (e: Exception) { + Log.e("Import", "Error importing", e) + onError("Import failed: ${e.message}") + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt new file mode 100644 index 0000000..9d39a8b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt @@ -0,0 +1,156 @@ +package net.helcel.cowspent.android.project + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R + +import net.helcel.cowspent.model.DBProject + +@Composable +fun ProjectOptionsDialogContent( + onEditProject: () -> Unit, + onRemoveProject: () -> Unit, + onManageMembers: () -> Unit, + onManageCurrencies: () -> Unit, + onStatistics: () -> Unit, + onSettle: () -> Unit, + onShareProject: () -> Unit, + onExportProject: () -> Unit, + onDismiss: () -> Unit, + isArchived: Boolean = false, + accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN, + isShareable: Boolean = true +) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.choose_project_management_action), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 8.dp) + ) + + val options = mutableListOf() + val isMaintainer = accessLevel >= DBProject.ACCESS_LEVEL_MAINTAINER || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN + val isParticipant = accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN + + if (!isArchived && isMaintainer) { + options.add(ProjectOption(stringResource(R.string.action_edit_project), Icons.Default.Edit, onEditProject)) + } + options.add(ProjectOption(stringResource(R.string.fab_rm_project), Icons.Default.Delete, onRemoveProject)) + if (!isArchived && isMaintainer) { + options.add(ProjectOption(stringResource(R.string.fab_manage_members), Icons.Default.Group, onManageMembers)) + options.add(ProjectOption(stringResource(R.string.fab_manage_currencies), Icons.Default.MonetizationOn, onManageCurrencies)) + } + options.add(ProjectOption(stringResource(R.string.fab_statistics), Icons.Default.BarChart, onStatistics)) + if (!isArchived && isParticipant) { + options.add(ProjectOption(stringResource(R.string.fab_settle), Icons.Default.Handshake, onSettle)) + } + if (isShareable && isParticipant) { + options.add(ProjectOption(stringResource(R.string.action_share_project), Icons.Default.Share, onShareProject)) + } + options.add(ProjectOption(stringResource(R.string.fab_export_project), Icons.Default.Download, onExportProject)) + + // Simple 2-column grid using Rows + for (i in options.indices step 2) { + Row(modifier = Modifier.fillMaxWidth()) { + ProjectOptionItem( + option = options[i], + modifier = Modifier.weight(1f) + ) + if (i + 1 < options.size) { + ProjectOptionItem( + option = options[i + 1], + modifier = Modifier.weight(1f) + ) + } else { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + } + } +} + +data class ProjectOption( + val title: String, + val icon: ImageVector, + val onClick: () -> Unit +) + +@Composable +fun ProjectOptionItem( + option: ProjectOption, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clickable { option.onClick() } + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = option.icon, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colors.primary + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = option.title, + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + maxLines = 2, + lineHeight = 14.sp + ) + } +} + +@Preview(showBackground = true) +@Composable +fun ProjectOptionsDialogPreview() { + MaterialTheme { + ProjectOptionsDialogContent( + onEditProject = {}, + onRemoveProject = {}, + onManageMembers = {}, + onManageCurrencies = {}, + onStatistics = {}, + onSettle = {}, + onShareProject = {}, + onExportProject = {}, + onDismiss = {}, + isArchived = false, + accessLevel = DBProject.ACCESS_LEVEL_ADMIN, + isShareable = true + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt new file mode 100644 index 0000000..ade833d --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt @@ -0,0 +1,316 @@ +package net.helcel.cowspent.android.project + +import android.content.Intent +import android.graphics.Bitmap +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.contentColorFor +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.QrCode +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri +import com.google.zxing.WriterException +import net.helcel.cowspent.R +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.util.ColorUtils + +@Composable +fun ProjectShareDialogContent( + proj: DBProject, + onShare: (String) -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val clipboardManager = LocalClipboardManager.current + val qrCodeLinkWarn = stringResource(R.string.qrcode_link_open_attempt_warning) + + val shareUrl = remember { proj.getShareUrl() } + val publicWebUrl = remember { proj.getPublicWebUrl() } + + val qrBitmap = remember(shareUrl) { + try { + ColorUtils.encodeAsBitmap(shareUrl) + } catch (_: WriterException) { + null + } + } + + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 650.dp) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 8.dp) + ) { + Icon( + Icons.Default.Share, + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.share_dialog_title), + style = MaterialTheme.typography.h6, + fontWeight = FontWeight.Bold + ) + } + + Column( + modifier = Modifier + .weight(1f, fill = false) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ShareCard( + title = stringResource(R.string.share_project_public_url_title), + url = publicWebUrl, + description = stringResource(R.string.share_project_public_url_dialog_message), + icon = Icons.Default.Link, + onUrlClick = { + val i = Intent(Intent.ACTION_VIEW).apply { + data = publicWebUrl.toUri() + } + context.startActivity(i) + }, + onCopyClick = { + clipboardManager.setText(AnnotatedString(publicWebUrl)) + Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show() + } + ) + + ShareCard( + title = stringResource(R.string.share_project_public_qrcode_title), + url = shareUrl, + description = stringResource(R.string.share_project_dialog_message), + icon = Icons.Default.QrCode, + qrBitmap = qrBitmap, + onUrlClick = { + Toast.makeText(context, qrCodeLinkWarn, Toast.LENGTH_SHORT).show() + }, + onCopyClick = { + clipboardManager.setText(AnnotatedString(shareUrl)) + Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show() + } + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + DialogActions( + onShare = { onShare(shareUrl) }, + onDismiss = onDismiss + ) + } + } +} + +@Composable +private fun ShareCard( + title: String, + url: String, + description: String, + icon: ImageVector, + onUrlClick: () -> Unit, + onCopyClick: () -> Unit, + qrBitmap: Bitmap? = null +) { + Surface( + shape = MaterialTheme.shapes.medium, + color = colorResource(R.color.fg_default_low).copy(alpha = 0.04f), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colors.primary.copy(alpha = 0.7f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = title, + style = MaterialTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.8f) + ) + } + + if (qrBitmap != null) { + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier.size(128.dp) + .clip(MaterialTheme.shapes.small) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + bitmap = qrBitmap.asImageBitmap(), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Surface( + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colors.surface, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .clickable { onUrlClick() } + .padding(start = 12.dp, end = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = url, + modifier = Modifier.weight(1f), + color = MaterialTheme.colors.primary, + style = MaterialTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Medium + ) + IconButton(onClick = onCopyClick) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy", + modifier = Modifier.size(18.dp), + tint = colorResource(R.color.fg_default_low) + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = description, + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + lineHeight = 16.sp, + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + } + } +} + +@Composable +private fun DialogActions( + onShare: () -> Unit, + onDismiss: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton( + onClick = onDismiss, + shape = MaterialTheme.shapes.small + ) { + Text( + text = stringResource(R.string.simple_ok).uppercase(), + style = MaterialTheme.typography.button, + fontWeight = FontWeight.SemiBold + ) + } + Button( + onClick = onShare, + shape = MaterialTheme.shapes.small, + elevation = null + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.simple_share_share).uppercase(), + style = MaterialTheme.typography.button, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun ProjectShareDialogContentPreview() { + MaterialTheme { + ProjectShareDialogContent( + proj = DBProject( + 1, "Vacation", "", "vacation", null, null, null, + ProjectType.LOCAL, 0L, null, false, 0, null + ), + onShare = {}, + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt new file mode 100644 index 0000000..92f58a6 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt @@ -0,0 +1,347 @@ +package net.helcel.cowspent.android.project.create + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Patterns +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.* +import androidx.core.net.toUri +import androidx.core.content.edit +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.android.main.MainConstants +import net.helcel.cowspent.android.helper.QrCodeScannerActivity +import net.helcel.cowspent.android.project.ProjectImportHelper + +class NewProjectActivity : AppCompatActivity() { + + private val viewModel: NewProjectViewModel by viewModels() + private lateinit var db: CowspentSQLiteOpenHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + db = CowspentSQLiteOpenHelper.getInstance(this) + + if (savedInstanceState == null) { + handleIntent(intent) + } + + setContent { + ThemeUtils.CowspentTheme { + NewProjectScreen( + viewModel = viewModel, + onScanQrCode = { + val createIntent = Intent(this, QrCodeScannerActivity::class.java) + scanQRCodeLauncher.launch(createIntent) + }, + onImportFile = { + val intent = Intent() + .setType("*/*") + .setAction(Intent.ACTION_GET_CONTENT) + importFileLauncher.launch(Intent.createChooser(intent, "Select a file")) + }, + onChooseFromNextcloud = { + chooseFromNextcloud() + }, + onOkPressed = { onPressOk() }, + onBack = { finish() } + ) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent) { + val defaultTypeId = intent.getStringExtra(PARAM_DEFAULT_PROJECT_TYPE) + if (defaultTypeId != null) { + viewModel.projectType = ProjectType.getTypeById(defaultTypeId) ?: ProjectType.LOCAL + } + + val defaultNcUrl = intent.getStringExtra(PARAM_DEFAULT_NC_URL) + if (defaultNcUrl != null) { + viewModel.defaultNcUrl = defaultNcUrl + if (viewModel.projectType != ProjectType.IHATEMONEY) { + viewModel.projectUrl = defaultNcUrl + } + } + + val defaultProjectId = intent.getStringExtra(PARAM_DEFAULT_PROJECT_ID) + if (defaultProjectId != null) { + viewModel.projectId = defaultProjectId + } + + val defaultPassword = intent.getStringExtra(PARAM_DEFAULT_PROJECT_PASSWORD) + if (defaultPassword != null) { + viewModel.projectPassword = defaultPassword + } + + if (Intent.ACTION_VIEW == intent.action) { + val data = intent.data + if (data != null) { + viewModel.updateFromUri(data) + if (viewModel.isFormValid()) { + onPressOk() + } + } + } + } + + private val scanQRCodeLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.getStringExtra(MainConstants.KEY_QR_CODE)?.let { scannedUrl -> + viewModel.updateFromUri(scannedUrl.toUri()) + if (viewModel.isFormValid()) { + onPressOk() + } + } + } + } + + private val importFileLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.data?.let { importFromFile(it) } + } + } + + private fun chooseFromNextcloud() { + lifecycleScope.launch { + val accountProjects = withContext(Dispatchers.IO) { db.accountProjects } + if (accountProjects.isEmpty()) { + showToast(getString(R.string.choose_account_project_dialog_impossible), Toast.LENGTH_LONG) + return@launch + } + + viewModel.nextcloudProjects = accountProjects + viewModel.showNextcloudProjectDialog = true + } + } + + private fun onPressOk() { + val type = viewModel.projectType + val todoCreate = viewModel.whatTodoIsCreate + val url = getFormattedUrl() + + val fakeProj = DBProject( + 0, "", "", "", url, + "", 0L, type, 0L, + null, false, DBProject.ACCESS_LEVEL_UNKNOWN, + "" + ) + + if (isValidUrl(url) && todoCreate && ProjectType.COSPEND == type && + db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj) && + !viewModel.showAuthWarningDialog // Avoid infinite loop + ) { + viewModel.showAuthWarningDialog = true + } else { + createProject() + } + } + + private fun getFormattedUrl(): String { + var url = viewModel.projectUrl.trim() + if (viewModel.projectType == ProjectType.COSPEND && !isCospendSchemeLink(url)) { + url = url.replace("/+$".toRegex(), "") + "/index.php/apps/cospend" + } + if (viewModel.projectType == ProjectType.IHATEMONEY) { + url = url.replace("/+$".toRegex(), "") + } + if (!url.startsWith("http://") && !url.startsWith("https://") && isValidUrl("https://$url")) { + url = "https://$url" + } + return url + } + + private fun isValidUrl(url: String): Boolean = Patterns.WEB_URL.matcher(url).matches() + + private fun isCospendSchemeLink(url: String): Boolean { + val data = url.toUri() + return (("cospend" == data.scheme || "cospend+http" == data.scheme || + "cowspent" == data.scheme || "cowspent+http" == data.scheme || + "ihatemoney" == data.scheme || "ihatemoney+http" == data.scheme) + && data.pathSegments.size >= 2) + } + + private fun createProject() { + val isCospendScheme = isCospendSchemeLink(getFormattedUrl()) + val rid = viewModel.projectId + if (!isCospendScheme && (rid == "" || rid.contains(",") || rid.contains("/"))) { + showToast(getString(R.string.error_invalid_project_remote_id), Toast.LENGTH_LONG) + return + } + + if (viewModel.projectType != ProjectType.LOCAL && !isCospendScheme) { + if (!isValidUrl(getFormattedUrl())) { + showToast("Invalid URL", Toast.LENGTH_SHORT) + return + } + val passwordRequired = !viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.IHATEMONEY + if (passwordRequired && viewModel.projectPassword.isEmpty()) { + showToast("Invalid password", Toast.LENGTH_SHORT) + return + } + } + + if (!viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.LOCAL) { + if (viewModel.projectType == ProjectType.LOCAL) { + val pid = saveLocalProject() + close(pid, false) + } else { + saveRemoteProject(false) + } + } else { + if (viewModel.projectName.isEmpty()) { + showToast("Invalid project name", Toast.LENGTH_SHORT) + return + } + if (!SupportUtil.isValidEmail(viewModel.projectEmail)) { + showToast("Invalid email", Toast.LENGTH_SHORT) + return + } + + viewModel.isCreatingRemoteProject = true + + if (!db.cowspentServerSyncHelper.createRemoteProject( + viewModel.projectId, viewModel.projectName, + viewModel.projectEmail, viewModel.projectPassword, getFormattedUrl(), viewModel.projectType, createRemoteCallBack + ) + ) { + viewModel.isCreatingRemoteProject = false + } + } + } + + private fun saveLocalProject(): Long { + val newProject = DBProject( + 0, viewModel.projectId, "", viewModel.projectId, null, + null, null, viewModel.projectType, 0L, + null, false, DBProject.ACCESS_LEVEL_UNKNOWN, + null + ) + return addProjectToDb(newProject) + } + + private fun saveRemoteProject(ignorePassword: Boolean) { + val newProject = getProjectFromFields(ignorePassword) + if (!db.cowspentServerSyncHelper.getRemoteProjectInfo(newProject, getRemoteInfoCallBack)) { + showToast(getString(R.string.error_no_network), Toast.LENGTH_LONG) + } + } + + private fun getProjectFromFields(ignorePassword: Boolean): DBProject { + var remoteId = viewModel.projectId + var url = getFormattedUrl() + var password = if (ignorePassword) "" else viewModel.projectPassword + + if (isCospendSchemeLink(url)) { + val data = url.toUri() + password = if (ignorePassword) "" else (data.lastPathSegment ?: "") + remoteId = data.pathSegments[data.pathSegments.size - 2] + val protocol = if (data.scheme?.endsWith("+http") == true) "http" else "https" + var path = protocol + "://" + data.host + (data.path ?: "").replace(("/$remoteId/$password$").toRegex(), "") + if (viewModel.projectType == ProjectType.COSPEND) { + path = path.replace("/+$".toRegex(), "") + "/index.php/apps/cospend" + } + url = path + } + + return DBProject( + 0, remoteId, password, viewModel.projectName, url, + viewModel.projectEmail, null, viewModel.projectType, 0L, + null, false, DBProject.ACCESS_LEVEL_UNKNOWN, + null + ) + } + + private fun addProjectToDb(project: DBProject): Long { + var pid = 0L + runBlocking { + pid = withContext(Dispatchers.IO) { db.addProject(project) } + } + PreferenceManager.getDefaultSharedPreferences(this).edit { + putLong( + "selected_project", + pid + ) + } + showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG) + return pid + } + + private val getRemoteInfoCallBack = object : ICallback { + override fun onFinish() {} + override fun onFinish(result: String, message: String) { + if (message.isEmpty()) { + val pid = addProjectToDb(getProjectFromFields(false)) + close(pid, true) + } else { + viewModel.errorDialogMessage = getString(R.string.error_project_connect_check, message) + } + } + override fun onScheduled() {} + } + + private val createRemoteCallBack = object : IProjectCreationCallback { + override fun onFinish(result: String, message: String, usePrivateApi: Boolean) { + if (message.isEmpty()) { + saveRemoteProject(usePrivateApi) + } else { + viewModel.errorDialogMessage = getString(R.string.error_create_remote_project_helper, message) + viewModel.isCreatingRemoteProject = false + } + } + } + + private fun close(pid: Long, justAdded: Boolean) { + val data = Intent() + if (justAdded) { + data.putExtra(MainConstants.ADDED_PROJECT, pid) + } else { + data.putExtra(MainConstants.CREATED_PROJECT, pid) + } + setResult(RESULT_OK, data) + finish() + } + + private fun showToast(text: CharSequence?, duration: Int) { + Toast.makeText(this, text, duration).show() + } + + private fun importFromFile(fileUri: Uri) { + ProjectImportHelper.importFromFile( + this, + db, + fileUri, + onSuccess = { pid -> close(pid, false) }, + onError = { message -> showToast(message, Toast.LENGTH_LONG) } + ) + } + + companion object { + const val PARAM_DEFAULT_NC_URL = "defaultNcUrl" + const val PARAM_DEFAULT_PROJECT_ID = "defaultProjectId" + const val PARAM_DEFAULT_PROJECT_PASSWORD = "defaultProjectPassword" + const val PARAM_DEFAULT_PROJECT_TYPE = "defaultProjectType" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt new file mode 100644 index 0000000..bceb66b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt @@ -0,0 +1,339 @@ +package net.helcel.cowspent.android.project.create + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.LibraryBooks +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.QrCode2 +import androidx.compose.material.icons.filled.Title +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.model.ProjectType + +@Composable +fun NewProjectScreen( + viewModel: NewProjectViewModel, + onScanQrCode: () -> Unit, + onImportFile: () -> Unit, + onChooseFromNextcloud: () -> Unit, + onOkPressed: () -> Unit, + onBack: () -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.action_add_project)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + }, + floatingActionButton = { + if (viewModel.isFormValid()) { + FloatingActionButton(onClick = onOkPressed) { + Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save_bill)) + } + } + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + .fillMaxSize() + ) { + // What to do + SectionRow( + label = stringResource(R.string.new_project_what_todo) + ) { + Row { + ToggleButton( + text = stringResource(R.string.todo_join_label), + selected = !viewModel.whatTodoIsCreate, + onClick = { + viewModel.whatTodoIsCreate = false + if (viewModel.projectType == ProjectType.LOCAL) + viewModel.projectType = ProjectType.COSPEND + } + ) + Spacer(modifier = Modifier.width(8.dp)) + ToggleButton( + text = stringResource(R.string.todo_create_label), + selected = viewModel.whatTodoIsCreate, + onClick = { viewModel.whatTodoIsCreate = true } + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + SectionRow( + label = stringResource(R.string.new_project_where) + ) { + Row { + if (viewModel.whatTodoIsCreate) { + ToggleButton( + text = stringResource(R.string.where_local_short), + selected = viewModel.projectType == ProjectType.LOCAL, + onClick = { viewModel.projectType = ProjectType.LOCAL } + ) + Spacer(modifier = Modifier.width(8.dp)) + } + ToggleButton( + text = stringResource(R.string.where_cospend_short), + selected = viewModel.projectType == ProjectType.COSPEND, + onClick = { viewModel.projectType = ProjectType.COSPEND } + ) + Spacer(modifier = Modifier.width(8.dp)) + ToggleButton( + text = stringResource(R.string.where_ihatemoney_short), + selected = viewModel.projectType == ProjectType.IHATEMONEY, + onClick = { viewModel.projectType = ProjectType.IHATEMONEY } + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + + if (viewModel.whatTodoIsCreate) { + if (viewModel.projectType == ProjectType.COSPEND) { + Button( + onClick = onChooseFromNextcloud + ) { + Text(stringResource(R.string.new_project_from_nextcloud_tooltip)) + } + } else if (viewModel.projectType == ProjectType.LOCAL) { + Button( + onClick = onImportFile + ) { + Text(stringResource(R.string.import_tooltip)) + } + } + } else { + Spacer(modifier = Modifier.width(16.dp)) + Button(onClick = onScanQrCode) { + Text(text = stringResource(R.string.scan_qrcode)) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + Icons.Default.QrCode2, + contentDescription = null, + modifier = Modifier.size(16.dp, 16.dp), + ) + } + } + + + Spacer(modifier = Modifier.height(16.dp)) + + if (viewModel.projectType != ProjectType.LOCAL) { + OutlinedTextField( + value = viewModel.projectUrl, + onValueChange = { viewModel.projectUrl = it }, + label = { + Text( + stringResource( + if (viewModel.projectType == ProjectType.COSPEND) R.string.setting_cospend_project_url + else R.string.setting_ihatemoney_project_url + ) + ) + }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + OutlinedTextField( + value = viewModel.projectId, + onValueChange = { viewModel.projectId = it }, + label = { Text(stringResource(R.string.setting_project_id)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) } + ) + + if (viewModel.projectType != ProjectType.LOCAL && (!viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.IHATEMONEY)) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = viewModel.projectPassword, + onValueChange = { viewModel.projectPassword = it }, + label = { Text(stringResource(R.string.setting_new_project_password)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) } + ) + } + + if (viewModel.whatTodoIsCreate && viewModel.projectType != ProjectType.LOCAL) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = viewModel.projectName, + onValueChange = { viewModel.projectName = it }, + label = { Text(stringResource(R.string.setting_new_project_name)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) } + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = viewModel.projectEmail, + onValueChange = { viewModel.projectEmail = it }, + label = { Text(stringResource(R.string.setting_new_project_email)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) } + ) + } + } + } + + if (viewModel.showAuthWarningDialog) { + AlertDialog( + onDismissRequest = { viewModel.showAuthWarningDialog = false }, + title = { Text(stringResource(R.string.auth_project_creation_title)) }, + text = { Text(stringResource(R.string.warning_auth_project_creation)) }, + confirmButton = { + TextButton(onClick = { + viewModel.showAuthWarningDialog = false + onOkPressed() + }) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { viewModel.showAuthWarningDialog = false }) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } + + if (viewModel.showNextcloudProjectDialog) { + AlertDialog( + onDismissRequest = { viewModel.showNextcloudProjectDialog = false }, + title = { Text(stringResource(R.string.choose_account_project_dialog_title)) }, + text = { + Column { + viewModel.nextcloudProjects.forEach { project -> + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = false, + onClick = { + viewModel.projectId = project.remoteId + viewModel.projectUrl = project.ncUrl + viewModel.showNextcloudProjectDialog = false + } + ) + .padding(16.dp) + ) { + Text(text = project.name) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = { viewModel.showNextcloudProjectDialog = false }) { + Text(stringResource(R.string.simple_cancel)) + } + } + ) + } + + if (viewModel.isCreatingRemoteProject) { + AlertDialog( + onDismissRequest = { }, + title = { Text(stringResource(R.string.simple_loading)) }, + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator() + Spacer(modifier = Modifier.width(16.dp)) + Text(stringResource(R.string.creating_remote_project)) + } + }, + confirmButton = {} + ) + } + + if (viewModel.errorDialogMessage != null) { + AlertDialog( + onDismissRequest = { viewModel.errorDialogMessage = null }, + title = { Text(stringResource(R.string.simple_error)) }, + text = { Text(viewModel.errorDialogMessage!!) }, + confirmButton = { + TextButton(onClick = { viewModel.errorDialogMessage = null }) { + Text(stringResource(R.string.simple_ok)) + } + } + ) + } +} + +@Composable +fun SectionRow( + label: String, + content: @Composable () -> Unit +) { + Row(verticalAlignment = Alignment.Top) { + Column { + Text(label, fontSize = 12.sp, color = MaterialTheme.colors.onSurface) + content() + } + } +} + +@Composable +fun ToggleButton( + text: String, + selected: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + colors = if (selected) { + ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary) + } else { + ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.surface) + } + ) { + Text( + text, + fontSize = 12.sp, + color = if (selected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface + ) + } +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview +@Composable +fun NewProjectScreenPreview() { + NewProjectScreen( + viewModel = NewProjectViewModel().apply { + whatTodoIsCreate = true + projectType = ProjectType.COSPEND + }, + onScanQrCode = {}, + onImportFile = {}, + onChooseFromNextcloud = {}, + onOkPressed = {}, + onBack = {} + ) +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectViewModel.kt new file mode 100644 index 0000000..b0060c0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectViewModel.kt @@ -0,0 +1,95 @@ +package net.helcel.cowspent.android.project.create + +import android.net.Uri +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.model.DBAccountProject +import net.helcel.cowspent.model.ProjectType + +class NewProjectViewModel : ViewModel() { + var whatTodoIsCreate by mutableStateOf(false) + + private var _projectType by mutableStateOf(ProjectType.COSPEND) + var projectType: ProjectType + get() = _projectType + set(value) { + _projectType = value + if (value == ProjectType.IHATEMONEY && (projectUrl.isEmpty() || projectUrl == defaultNcUrl)) { + projectUrl = "https://ihatemoney.org" + } else if (value == ProjectType.COSPEND && (projectUrl == "https://ihatemoney.org" || projectUrl.isEmpty())) { + projectUrl = defaultNcUrl + } + } + + var projectUrl by mutableStateOf("") + var defaultNcUrl by mutableStateOf("") + var projectId by mutableStateOf("") + var projectPassword by mutableStateOf("") + var projectName by mutableStateOf("") + var projectEmail by mutableStateOf("") + + var showAuthWarningDialog by mutableStateOf(false) + var showNextcloudProjectDialog by mutableStateOf(false) + var nextcloudProjects by mutableStateOf>(emptyList()) + + var isCreatingRemoteProject by mutableStateOf(false) + var errorDialogMessage by mutableStateOf(null) + + fun isFormValid(): Boolean { + if (whatTodoIsCreate) { + if (projectId.isEmpty()) return false + if (projectType != ProjectType.LOCAL) { + if (projectUrl.isEmpty()) return false + if (projectName.isEmpty()) return false + if (projectEmail.isEmpty()) return false + if (projectType == ProjectType.IHATEMONEY && projectPassword.isEmpty()) return false + } + } else { + // Join + if (projectType == ProjectType.LOCAL) return false + if (projectId.isEmpty() && !isCospendSchemeLink(projectUrl)) return false + if (projectUrl.isEmpty() && !isCospendSchemeLink(projectUrl)) return false + if (projectPassword.isEmpty() && !isCospendSchemeLink(projectUrl)) return false + } + return true + } + + private fun isCospendSchemeLink(url: String): Boolean { + return url.startsWith("cospend://") || url.startsWith("cospend+http://") || + url.startsWith("cowspent://") || url.startsWith("cowspent+http://") || + url.startsWith("ihatemoney://") || url.startsWith("ihatemoney+http://") + } + + fun updateFromUri(data: Uri) { + if ((data.scheme == "cospend" || data.scheme == "cospend+http" || + data.scheme == "cowspent" || data.scheme == "cowspent+http" || + data.scheme == "ihatemoney" || data.scheme == "ihatemoney+http") + && data.pathSegments.isNotEmpty() + ) { + val password: String + val pid: String + if (data.path!!.endsWith("/")) { + password = "" + pid = data.lastPathSegment!! + } else { + password = data.lastPathSegment!! + pid = data.pathSegments[data.pathSegments.size - 2] + } + var protocol = "https" + if (data.scheme == "cospend+http" || data.scheme == "cowspent+http" || data.scheme == "ihatemoney+http") { + protocol = "http" + } + val url = protocol + "://" + data.host + val port = if (data.port != -1) ":${data.port}" else "" + val fullUrl = "$url$port${data.path!!.replace(("/$pid/$password$").toRegex(), "")}" + + projectPassword = password + projectId = pid + projectUrl = fullUrl + projectType = if (data.scheme?.startsWith("ihatemoney") == true) ProjectType.IHATEMONEY else ProjectType.COSPEND + whatTodoIsCreate = false + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt new file mode 100644 index 0000000..a105eb8 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt @@ -0,0 +1,192 @@ +package net.helcel.cowspent.android.project.edit + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.showToast +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.ICallback +import net.helcel.cowspent.util.SupportUtil +import net.helcel.cowspent.android.main.MainConstants + + +class EditProjectActivity : AppCompatActivity() { + + private val viewModel: EditProjectViewModel by viewModels() + private lateinit var db: CowspentSQLiteOpenHelper + private lateinit var project: DBProject + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + db = CowspentSQLiteOpenHelper.getInstance(this) + + val id = intent.getLongExtra(PARAM_PROJECT_ID, 0) + if (id <= 0) { + finish() + return + } + + lifecycleScope.launch { + project = withContext(Dispatchers.IO) { db.getProject(id) }!! + viewModel.initFromProject(project) + + setContent { + ThemeUtils.CowspentTheme { + EditProjectScreen( + viewModel = viewModel, + onSave = { onSave() }, + onDeleteRemote = { onDeleteRemote() }, + onBack = { finish() } + ) + } + } + } + } + + private fun onSave() { + val currentPwd = viewModel.password + val newPwd = viewModel.newPassword + val newName = viewModel.name + val newEmail = viewModel.email + + if (newName.isEmpty()) { + showToast(this, getString(R.string.error_invalid_project_name), Toast.LENGTH_LONG) + return + } + if (newEmail.isNotEmpty() && !SupportUtil.isValidEmail(newEmail)) { + showToast(this, getString(R.string.error_invalid_email), Toast.LENGTH_LONG) + return + } + + val nameChanged = newName != project.name + val emailChanged = newEmail != project.email + val pwdChanged = newPwd != project.password + val currentPwdChanged = currentPwd != project.password + + if (!nameChanged && !emailChanged && !pwdChanged && !currentPwdChanged) { + showToast(this, getString(R.string.project_edition_no_change), Toast.LENGTH_LONG) + return + } + + if (project.isLocal) { + val targetPwd = if (pwdChanged) newPwd else currentPwd + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db.updateProject( + project.id, newName, newEmail, targetPwd, + null, project.type, null, + null, null, + null, null + ) + } + closeOnEdit(project.id) + } + return + } + + // Remote project + if (nameChanged || emailChanged || pwdChanged) { + // Update local password first if currentPwd was changed (user fixing credentials) + if (currentPwdChanged) { + project.password = currentPwd + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db.updateProject(project.id, null, null, currentPwd, null, project.type, null, null, null, null, null) + } + } + } + + if (!db.cowspentServerSyncHelper.editRemoteProject( + project.id, + newName, + newEmail, + if (pwdChanged) newPwd else null, + null, + editCallBack + ) + ) { + showToast(this, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG) + } + } else { + // Only current password changed locally (fixing credentials) + lifecycleScope.launch { + withContext(Dispatchers.IO) { + db.updateProject( + project.id, null, null, currentPwd, + null, project.type, null, + null, null, + null, null + ) + } + closeOnEdit(project.id) + } + } + } + + private fun onDeleteRemote() { + viewModel.showDialog( + message = getString(R.string.confirm_delete_project_dialog_title), + positiveText = getString(R.string.simple_yes), + onConfirm = { + if (!db.cowspentServerSyncHelper.deleteRemoteProject(project.id, deleteCallBack)) { + showToast(this, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG) + } + }, + negativeText = getString(R.string.simple_no) + ) + } + + private val editCallBack = object : ICallback { + override fun onFinish() {} + override fun onFinish(result: String, message: String) { + if (message.isEmpty()) { + closeOnEdit(project.id) + } else { + showToast(this@EditProjectActivity, getString(R.string.error_edit_remote_project_helper, message), Toast.LENGTH_LONG) + } + } + override fun onScheduled() {} + } + + private val deleteCallBack = object : ICallback { + override fun onFinish() {} + override fun onFinish(result: String, message: String) { + if (message.isEmpty()) { + closeOnDelete(result.toLong()) + } else { + showToast(this@EditProjectActivity, getString(R.string.error_edit_remote_project_helper, message), Toast.LENGTH_LONG) + } + } + override fun onScheduled() {} + } + + private fun closeOnDelete(projId: Long) { + val data = Intent() + data.putExtra(MainConstants.DELETED_PROJECT, projId) + setResult(RESULT_OK, data) + finish() + } + + private fun closeOnEdit(projId: Long) { + val data = Intent() + data.putExtra(MainConstants.EDITED_PROJECT, projId) + setResult(RESULT_OK, data) + finish() + } + + + + companion object { + const val PARAM_PROJECT_ID = "projectId" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt new file mode 100644 index 0000000..350568b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt @@ -0,0 +1,145 @@ +package net.helcel.cowspent.android.project.edit + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.LockOpen +import androidx.compose.material.icons.filled.Title +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.AlertDialog + +@Composable +fun EditProjectScreen( + viewModel: EditProjectViewModel, + onSave: () -> Unit, + onDeleteRemote: () -> Unit, + onBack: () -> Unit +) { + val dialogState = viewModel.dialogState + if (dialogState != null) { + AlertDialog( + showDialog = true, + onDismissRequest = { viewModel.dismissDialog() }, + title = dialogState.title, + message = dialogState.message, + icon = dialogState.icon, + items = dialogState.items, + positiveText = dialogState.positiveText, + negativeText = dialogState.negativeText, + neutralText = dialogState.neutralText, + onConfirm = { + dialogState.onConfirm?.invoke() + viewModel.dismissDialog() + }, + onCancel = { + dialogState.onCancel?.invoke() + viewModel.dismissDialog() + }, + onNeutral = { + dialogState.onNeutral?.invoke() + viewModel.dismissDialog() + } + ) { + dialogState.onItemSelected?.invoke(it) + viewModel.dismissDialog() + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.simple_edit_project)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + IconButton(onClick = onDeleteRemote) { + Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.menu_delete_project_remote)) + } + }, + backgroundColor = MaterialTheme.colors.surface, + elevation = 0.dp + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onSave) { + Icon(Icons.Default.Done, contentDescription = stringResource(R.string.menu_save_project)) + } + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(16.dp) + .fillMaxSize() + ) { + OutlinedTextField( + value = viewModel.name, + onValueChange = { viewModel.name = it }, + label = { Text(stringResource(R.string.setting_new_project_name)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = viewModel.password, + onValueChange = { viewModel.password = it }, + label = { Text(stringResource(R.string.setting_password)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = viewModel.newPassword, + onValueChange = { viewModel.newPassword = it }, + label = { Text(stringResource(R.string.setting_new_project_password)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.LockOpen, contentDescription = null) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = viewModel.email, + onValueChange = { viewModel.email = it }, + label = { Text(stringResource(R.string.setting_new_project_email)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) } + ) + } + } +} + +@SuppressLint("ViewModelConstructorInComposable") +@Preview(showBackground = true) +@Composable +fun EditProjectScreenPreview() { + MaterialTheme { + EditProjectScreen( + viewModel = EditProjectViewModel().apply { + name = "My Awesome Project" + email = "user@example.com" + }, + onSave = {}, + onDeleteRemote = {}, + onBack = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt new file mode 100644 index 0000000..fac868d --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt @@ -0,0 +1,57 @@ +package net.helcel.cowspent.android.project.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.ViewModel +import net.helcel.cowspent.android.helper.DialogState +import net.helcel.cowspent.model.DBProject + +class EditProjectViewModel : ViewModel() { + var name by mutableStateOf("") + var password by mutableStateOf("") + var newPassword by mutableStateOf("") + var email by mutableStateOf("") + + var dialogState by mutableStateOf(null) + + fun showDialog( + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + items: List? = null, + positiveText: String? = null, + negativeText: String? = null, + neutralText: String? = null, + onConfirm: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onNeutral: (() -> Unit)? = null, + onItemSelected: ((Int) -> Unit)? = null + ) { + dialogState = DialogState( + title = title, + message = message, + icon = icon, + items = items, + positiveText = positiveText, + negativeText = negativeText, + neutralText = neutralText, + onConfirm = onConfirm, + onCancel = onCancel, + onNeutral = onNeutral, + onItemSelected = onItemSelected + ) + } + + fun dismissDialog() { + dialogState = null + } + + fun initFromProject(project: DBProject) { + name = if (project.name == "null") "" else project.name + password = project.password + newPassword = project.password + email = project.email?.let { if (it == "null") "" else it } ?: "" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt new file mode 100644 index 0000000..0cfd11d --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt @@ -0,0 +1,70 @@ +package net.helcel.cowspent.android.project.member + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import net.helcel.cowspent.R + +@Composable +fun MemberAddDialogContent( + onAdd: (String) -> Unit, + onDismiss: () -> Unit +) { + var name by remember { mutableStateOf("") } + + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.add_member_dialog_title), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text(stringResource(R.string.member_edit_name)) }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), + singleLine = true + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.simple_cancel).uppercase()) + } + TextButton(onClick = { + if (name.isNotEmpty()) { + onAdd(name) + } + }) { + Text(stringResource(R.string.simple_ok).uppercase()) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun MemberAddDialogContentPreview() { + MaterialTheme { + MemberAddDialogContent(onAdd = {}, onDismiss = {}) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt new file mode 100644 index 0000000..656bf80 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt @@ -0,0 +1,208 @@ +package net.helcel.cowspent.android.project.member + +import android.graphics.Color +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.material.icons.filled.* +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.ColorPicker +import net.helcel.cowspent.android.helper.TextDrawable +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.util.ColorUtils + +@Composable +fun MemberEditDialogContent( + member: DBMember, + onSave: (String, Double, Boolean, Int?, Int?, Int?) -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit +) { + var name by remember { mutableStateOf(member.name) } + var weight by remember { mutableStateOf(member.weight.toString()) } + var isActivated by remember { mutableStateOf(member.isActivated) } + + val initialColor = remember { + if (member.r != null && member.g != null && member.b != null) { + Color.rgb(member.r!!, member.g!!, member.b!!) + } else { + TextDrawable.getColorFromName(member.name) + } + } + var selectedColor by remember { mutableIntStateOf(initialColor) } + var showColorPicker by remember { mutableStateOf(false) } + + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = stringResource(R.string.edit_member_dialog_title), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + // Name + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text(stringResource(R.string.member_edit_name)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Weight + OutlinedTextField( + value = weight, + onValueChange = { weight = it }, + label = { Text(stringResource(R.string.member_edit_weight)) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon(Icons.Default.LineWeight, contentDescription = null) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Activated + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { isActivated = !isActivated } + .padding(vertical = 8.dp) + ) { + Icon(Icons.Default.Block, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + Text(stringResource(R.string.member_edit_toggle), modifier = Modifier.weight(1f)) + Checkbox(checked = isActivated, onCheckedChange = { isActivated = it }) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Color + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + Icon(Icons.Default.Palette, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + Text(stringResource(R.string.member_edit_color), modifier = Modifier.weight(1f)) + Box( + modifier = Modifier + .size(40.dp) + .background(androidx.compose.ui.graphics.Color(selectedColor)) + .clickable { showColorPicker = true } + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + tint = if (ColorUtils.isLightColor(selectedColor)) androidx.compose.ui.graphics.Color.Black else androidx.compose.ui.graphics.Color.White + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Delete + Button( + onClick = onDelete, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Icon(Icons.Default.Delete, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.member_edit_delete)) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Buttons + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.simple_cancel).uppercase()) + } + TextButton(onClick = { + val w = weight.replace(',', '.').toDoubleOrNull() + if (name.isNotEmpty() && w != null) { + onSave( + name, + w, + isActivated, + Color.red(selectedColor), + Color.green(selectedColor), + Color.blue(selectedColor) + ) + } + }) { + Text(stringResource(R.string.simple_ok).uppercase()) + } + } + } + } + + if (showColorPicker) { + AlertDialog( + onDismissRequest = { showColorPicker = false }, + title = { Text(stringResource(R.string.settings_colorpicker_title)) }, + text = { + ColorPicker(initialColor = selectedColor) { + selectedColor = it + } + }, + confirmButton = { + TextButton(onClick = { showColorPicker = false }) { + Text(stringResource(R.string.simple_ok).uppercase()) + } + }, + dismissButton = { + TextButton(onClick = { showColorPicker = false }) { + Text(stringResource(R.string.simple_cancel).uppercase()) + } + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun MemberEditDialogContentPreview() { + MaterialTheme { + MemberEditDialogContent( + member = DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null), + onSave = { _, _, _, _, _, _ -> }, + onDelete = {}, + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt new file mode 100644 index 0000000..d2121f3 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt @@ -0,0 +1,132 @@ +package net.helcel.cowspent.android.project.member + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.contentColorFor +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.UserAvatar +import net.helcel.cowspent.android.helper.lazyVerticalScrollbar +import net.helcel.cowspent.model.DBMember + +@Composable +fun MemberManagementDialogContent( + members: List, + onAddMember: () -> Unit, + onEditMember: (DBMember) -> Unit, + onDismiss: () -> Unit +) { + val listState = rememberLazyListState() + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 500.dp) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.fab_manage_members), + style = MaterialTheme.typography.h6 + ) + IconButton(onClick = onAddMember) { + Icon(Icons.Default.Add,modifier=Modifier.size(32.dp), + contentDescription = stringResource(R.string.fab_add_member)) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + LazyColumn( + state = listState, + modifier = Modifier + .weight(1f, fill = false) + .lazyVerticalScrollbar(listState) + ) { + items(members) { member -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onEditMember(member) } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + UserAvatar( + name = member.name, + r = member.r, + g = member.g, + b = member.b, + avatar = member.avatar, + disabled = !member.isActivated, + size = 40.dp + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = member.name, + style = MaterialTheme.typography.body1 + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.simple_close).uppercase()) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun MemberManagementDialogContentPreview() { + MaterialTheme { + MemberManagementDialogContent( + members = listOf( + DBMember(1, 0, 1, "Alice", true, 1.0, 0, 255, 100, 100, null, null), + DBMember(2, 0, 1, "Bob", true, 1.0, 0, 100, 255, 100, null, null), + DBMember(3, 0, 1, "Charlie", false, 1.0, 0, 100, 100, 255, null, null) + ), + onAddMember = {}, + onEditMember = {}, + onDismiss = {} + ) + } +} + diff --git a/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt new file mode 100644 index 0000000..bcb6f04 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt @@ -0,0 +1,401 @@ +package net.helcel.cowspent.android.project.settle + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.contentColorFor +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.CenterFocusStrong +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.Transaction +import net.helcel.cowspent.model.UserItem +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.SupportUtil +import net.helcel.cowspent.util.SupportUtil.SETTLE_OPTIMAL +import net.helcel.cowspent.util.SupportUtil.settleBills + +@Composable +fun ProjectSettlementDialogContent( + proj: DBProject, + db: CowspentSQLiteOpenHelper, + onSettleBills: (List) -> Unit, + onShare: (List, Map) -> Unit, + onDismiss: () -> Unit +) { + val centerNoneStr = stringResource(R.string.center_none) + val memberList = remember(proj.id) { db.getMembersOfProject(proj.id, null) } + val userList = remember(memberList, centerNoneStr) { + buildList { + add(UserItem(SETTLE_OPTIMAL, centerNoneStr)) + addAll(memberList.map { UserItem(it.id, it.name) }) + } + } + + val membersBalance = remember(proj.id) { + val balance = mutableMapOf() + SupportUtil.getStatsOfProject( + proj.id, db, + mutableMapOf(), balance, mutableMapOf(), mutableMapOf(), + -1000, -1000, null, null + ) + balance + } + + val membersSortedByName = remember(proj.id) { + db.getMembersOfProject(proj.id, CowspentSQLiteOpenHelper.key_name) + } + + val memberIdToName = remember(membersSortedByName) { + membersSortedByName.associate { it.id to it.name } + } + + var selectedMemberId by remember(userList) { + mutableLongStateOf(userList.firstOrNull()?.id ?: SETTLE_OPTIMAL) + } + + val transactions = remember(selectedMemberId, membersBalance, membersSortedByName) { + settleBills(membersSortedByName, membersBalance, selectedMemberId) + } + + ProjectSettlementUI( + transactions = transactions, + userList = userList, + selectedMemberId = selectedMemberId, + memberIdToName = memberIdToName, + onMemberSelected = { selectedMemberId = it }, + onSettleBills = onSettleBills, + onShare = onShare, + onDismiss = onDismiss + ) +} + +@Composable +fun ProjectSettlementUI( + transactions: List, + userList: List, + selectedMemberId: Long, + memberIdToName: Map, + onMemberSelected: (Long) -> Unit, + onSettleBills: (List) -> Unit, + onShare: (List, Map) -> Unit, + onDismiss: () -> Unit +) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 480.dp) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.settle_dialog_title), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (transactions.isEmpty()) { + BalancedStateMessage() + } else { + MemberSelector( + selectedMemberId = selectedMemberId, + userList = userList, + onMemberSelected = onMemberSelected + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TransactionTable( + transactions = transactions, + memberIdToName = memberIdToName, + modifier = Modifier.weight(1f, fill = false) + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + ActionButtons( + transactions = transactions, + memberIdToName = memberIdToName, + onShare = onShare, + onSettleBills = onSettleBills, + onDismiss = onDismiss + ) + } + } +} + +@Composable +private fun BalancedStateMessage() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.settle_dialog_balanced), + style = MaterialTheme.typography.body1 + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun MemberSelector( + selectedMemberId: Long, + userList: List, + onMemberSelected: (Long) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + Surface( + onClick = { expanded = true }, + shape = MaterialTheme.shapes.medium, + color = colorResource(R.color.fg_default_low).copy(alpha = 0.05f), + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp) + ) { + Icon( + Icons.Default.CenterFocusStrong, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = colorResource(R.color.fg_default_low) + ) + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = userList.find { it.id == selectedMemberId }?.name ?: "", + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + + Icon( + Icons.Default.ArrowDropDown, + contentDescription = null, + tint = colorResource(R.color.fg_default_low) + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + userList.forEach { user -> + DropdownMenuItem(onClick = { + onMemberSelected(user.id) + expanded = false + }) { + Text( + text = user.name, + style = MaterialTheme.typography.body1 + ) + } + } + } + } + } +} + +@Composable +private fun TransactionTable( + transactions: List, + memberIdToName: Map, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + TableHeaderText(stringResource(R.string.settle_who), Modifier.weight(1.2f)) + TableHeaderText(stringResource(R.string.settle_to_whom), Modifier.weight(1.2f)) + TableHeaderText(stringResource(R.string.settle_how_much), Modifier.weight(1f), TextAlign.End) + } + + Divider(color = colorResource(R.color.fg_default_low).copy(alpha = 0.12f)) + + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(transactions) { t -> + TransactionRow(t, memberIdToName) + } + } + } +} + +@Composable +private fun TableHeaderText( + text: String, + modifier: Modifier = Modifier, + textAlign: TextAlign = TextAlign.Start +) { + Text( + text = text.uppercase(), + modifier = modifier, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.overline, + color = colorResource(R.color.fg_default_low), + textAlign = textAlign, + letterSpacing = 0.8.sp + ) +} + +@Composable +private fun TransactionRow( + transaction: Transaction, + memberIdToName: Map +) { + Surface( + shape = MaterialTheme.shapes.medium, + color = Color.Transparent + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = memberIdToName[transaction.owerMemberId] ?: "-", + modifier = Modifier.weight(1.2f), + style = MaterialTheme.typography.body2, + color = colorResource(R.color.fg_default) + ) + Text( + text = memberIdToName[transaction.receiverMemberId] ?: "-", + modifier = Modifier.weight(1.2f), + style = MaterialTheme.typography.body2, + color = colorResource(R.color.fg_default) + ) + Text( + text = SupportUtil.normalNumberFormat.format(transaction.amount), + modifier = Modifier.weight(1f), + textAlign = TextAlign.End, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + color = colorResource(R.color.fg_default) + ) + } + } +} + +@Composable +private fun ActionButtons( + transactions: List, + memberIdToName: Map, + onShare: (List, Map) -> Unit, + onSettleBills: (List) -> Unit, + onDismiss: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically + ) { + if (transactions.isNotEmpty()) { + TextButton(onClick = { onShare(transactions, memberIdToName) }) { + Text(stringResource(R.string.simple_settle_share).uppercase()) + } + TextButton(onClick = { onSettleBills(transactions) }) { + Text(stringResource(R.string.simple_create_bills).uppercase()) + } + } + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.simple_ok).uppercase()) + } + } +} + +@Preview(showBackground = true, name = "With Transactions") +@Composable +fun ProjectSettlementUIPreview() { + MaterialTheme { + ProjectSettlementUI( + transactions = listOf( + Transaction(1, 2, 25.50), + Transaction(3, 1, 10.00), + Transaction(2, 3, 5.25) + ), + userList = listOf( + UserItem(SETTLE_OPTIMAL, "None (Optimal)"), + UserItem(1, "Alice"), + UserItem(2, "Bob"), + UserItem(3, "Charlie") + ), + selectedMemberId = SETTLE_OPTIMAL, + memberIdToName = mapOf(1L to "Alice", 2L to "Bob", 3L to "Charlie"), + onMemberSelected = {}, + onSettleBills = {}, + onShare = { _, _ -> }, + onDismiss = {} + ) + } +} + +@Preview(showBackground = true, name = "Balanced") +@Composable +fun ProjectSettlementUIBalancedPreview() { + MaterialTheme { + ProjectSettlementUI( + transactions = emptyList(), + userList = listOf( + UserItem(SETTLE_OPTIMAL, "None (Optimal)"), + UserItem(1, "Alice"), + UserItem(2, "Bob") + ), + selectedMemberId = SETTLE_OPTIMAL, + memberIdToName = mapOf(1L to "Alice", 2L to "Bob"), + onMemberSelected = {}, + onSettleBills = {}, + onShare = { _, _ -> }, + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt new file mode 100644 index 0000000..854f23f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt @@ -0,0 +1,71 @@ +package net.helcel.cowspent.android.settings + +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.app.NavUtils +import androidx.preference.PreferenceManager +import net.helcel.cowspent.android.about.AboutActivity +import net.helcel.cowspent.android.account.AccountActivity +import net.helcel.cowspent.theme.ThemeUtils +import net.helcel.cowspent.util.ColorUtils + +/** + * Allows to change application settings. + */ +class PreferencesActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + NavUtils.navigateUpFromSameTask(this@PreferencesActivity) + } + }) + + setResult(RESULT_CANCELED) + + setContent { + val context = LocalContext.current + val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } + val nightModeKey = stringResource(net.helcel.cowspent.R.string.pref_key_night_mode) + + var appColor by remember { + mutableIntStateOf(ColorUtils.primaryColor(context)) + } + var nightMode by remember { + mutableStateOf(sharedPreferences.getString(nightModeKey, "-1") ?: "-1") + } + + val isDarkTheme = when (nightMode) { + "1" -> false + "2" -> true + else -> androidx.compose.foundation.isSystemInDarkTheme() + } + + ThemeUtils.CowspentTheme(accentColor = appColor, darkTheme = isDarkTheme) { + SettingsScreen( + onBack = { NavUtils.navigateUpFromSameTask(this) }, + onAccountSettingsClick = { + startActivity(Intent(this, AccountActivity::class.java)) + }, + onAboutClick = { + startActivity(Intent(this, AboutActivity::class.java)) + }, + onColorSelected = { appColor = it }, + onNightModeChanged = { nightMode = it } + ) + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt new file mode 100644 index 0000000..b84290b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt @@ -0,0 +1,449 @@ +package net.helcel.cowspent.android.settings + +import android.graphics.Color +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.AlertDialog +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Archive +import androidx.compose.material.icons.filled.Brightness2 +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.Sync +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.ColorPicker +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.ColorUtils +import net.helcel.cowspent.util.Cowspent + +@Composable +fun SettingsScreen( + onBack: () -> Unit, + onAccountSettingsClick: () -> Unit, + onAboutClick: () -> Unit, + onColorSelected: (Int) -> Unit, + onNightModeChanged: (String) -> Unit = {} +) { + val context = LocalContext.current + val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } + + val keyNightMode = stringResource(R.string.pref_key_night_mode) + val valNightModeSystem = stringResource(R.string.pref_value_night_mode_system) + val valNightModeNo = stringResource(R.string.pref_value_night_mode_no) + val valNightModeYes = stringResource(R.string.pref_value_night_mode_yes) + + val keyColorMode = stringResource(R.string.pref_key_color_mode) + val keyUseServerColor = stringResource(R.string.pref_key_use_server_color) + val keyUseSystemColor = stringResource(R.string.pref_key_use_system_color) + val keyColor = stringResource(R.string.pref_key_color) + val keyOfflineMode = stringResource(R.string.pref_key_offline_mode) + val keyShowArchived = stringResource(R.string.pref_key_show_archived) + + // States for preferences + var nightMode by remember(keyNightMode) { + mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1") + } + + var colorMode by remember(keyColorMode, keyUseServerColor, keyUseSystemColor) { + mutableStateOf(sharedPreferences.getString(keyColorMode, null) ?: run { + val useServer = sharedPreferences.getBoolean(keyUseServerColor, true) + val useSystem = sharedPreferences.getBoolean(keyUseSystemColor, true) + when { + useServer -> "server" + useSystem -> "system" + else -> "manual" + } + }) + } + + // Apply theme globally only when leaving settings to avoid flickering/restarts during selection + DisposableEffect(nightMode) { + onDispose { + Cowspent.setAppTheme(nightMode.toInt()) + } + } + + var appColor by remember(keyColor) { + mutableIntStateOf(sharedPreferences.getInt(keyColor, Color.BLUE)) + } + var offlineMode by remember(keyOfflineMode) { + mutableStateOf(sharedPreferences.getBoolean(keyOfflineMode, false)) + } + var showArchived by remember(keyShowArchived) { + mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false)) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.action_settings)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + // Appearance + SettingsCategory(stringResource(R.string.settings_appearance_category)) + + SettingsSwitchPreference( + title = stringResource(R.string.setting_show_archived), + icon = Icons.Default.Archive, + checked = showArchived, + onCheckedChange = { + showArchived = it + sharedPreferences.edit { + putBoolean(keyShowArchived, it) + if (!it) { + val selectedProjectId = sharedPreferences.getLong("selected_project", 0) + if (selectedProjectId != 0L) { + val db = CowspentSQLiteOpenHelper.getInstance(context) + val project = db.getProject(selectedProjectId) + if (project?.isArchived == true) { + putLong("selected_project", 0) + } + } + } + } + } + ) + + SettingsListPreference( + title = stringResource(R.string.settings_night_mode_title), + icon = Icons.Default.Brightness2, + value = nightMode, + entries = mapOf( + valNightModeSystem to stringResource(R.string.pref_value_theme_system), + valNightModeNo to stringResource(R.string.pref_value_theme_light), + valNightModeYes to stringResource(R.string.pref_value_theme_dark) + ), + onValueChange = { + nightMode = it + sharedPreferences.edit { + putString(keyNightMode, it) + } + onNightModeChanged(it) + } + ) + + SettingsListPreference( + title = stringResource(R.string.settings_color_mode_title), + icon = Icons.Default.Palette, + value = colorMode, + entries = mapOf( + "system" to stringResource(R.string.pref_value_color_system), + "server" to stringResource(R.string.pref_value_color_server), + "manual" to stringResource(R.string.pref_value_color_manual) + ), + onValueChange = { mode -> + colorMode = mode + sharedPreferences.edit { + putString(keyColorMode, mode) + // Keep legacy flags in sync just in case + putBoolean(keyUseServerColor, mode == "server") + putBoolean(keyUseSystemColor, mode == "system") + } + if (mode == "server") { + CowspentSQLiteOpenHelper.getInstance(context).cowspentServerSyncHelper.runAccountProjectsSync() + } + onColorSelected(ColorUtils.primaryColor(context)) + } + ) + + if (colorMode == "manual") { + SettingsColorPreference( + title = stringResource(R.string.settings_color_title), + summary = stringResource(R.string.settings_color_summary), + icon = Icons.Default.Palette, + initialColor = appColor, + onColorSelected = { + appColor = it + sharedPreferences.edit { + putInt(keyColor, it) + } + onColorSelected(it) + } + ) + } + + // Network + SettingsCategory(stringResource(R.string.settings_network_category)) + + SettingsSwitchPreference( + title = stringResource(R.string.settings_offline_mode_title), + summary = stringResource(R.string.settings_offline_mode_summary), + icon = Icons.Default.Sync, + checked = offlineMode, + onCheckedChange = { + offlineMode = it + sharedPreferences.edit { + putBoolean(keyOfflineMode, it) + } + } + ) + + SettingsPreference( + title = stringResource(R.string.settings_server_settings), + icon = Icons.Default.AccountCircle, + onClick = onAccountSettingsClick + ) + + // Other + SettingsCategory(stringResource(R.string.settings_other_category)) + + SettingsPreference( + title = stringResource(R.string.settings_about), + icon = Icons.Default.Info, + onClick = onAboutClick + ) + } + } +} + +@Composable +fun SettingsCategory(title: String) { + Text( + text = title.uppercase(), + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), + color = MaterialTheme.colors.primary, + style = MaterialTheme.typography.overline, + fontWeight = FontWeight.Bold + ) +} + +@Composable +fun SettingsPreference( + title: String, + summary: String? = null, + icon: Any? = null, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SettingsIcon(icon) + Spacer(modifier = Modifier.width(32.dp)) + Column { + Text(text = title, style = MaterialTheme.typography.subtitle1) + if (summary != null) { + Text(text = summary, style = MaterialTheme.typography.caption) + } + } + } +} + +@Composable +fun SettingsSwitchPreference( + title: String, + summary: String? = null, + icon: Any? = null, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onCheckedChange(!checked) } + .padding(16.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SettingsIcon(icon) + Spacer(modifier = Modifier.width(32.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.subtitle1) + if (summary != null) { + Text(text = summary, style = MaterialTheme.typography.caption) + } + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + uncheckedThumbColor = MaterialTheme.colors.onSurface + ) + ) + } +} + +@Composable +fun SettingsListPreference( + title: String, + icon: Any? = null, + value: String, + entries: Map, + onValueChange: (String) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + + SettingsPreference( + title = title, + summary = entries[value], + icon = icon, + onClick = { showDialog = true } + ) + + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text(title) }, + text = { + Column { + entries.forEach { (key, label) -> + Row( + Modifier.fillMaxWidth() + .selectable( + selected = (key == value), + onClick = { + onValueChange(key) + showDialog = false + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (key == value), + onClick = { + onValueChange(key) + showDialog = false + } + ) + Spacer(Modifier.width(8.dp)) + Text(text = label) + } + } + } + }, + confirmButton = { + TextButton(onClick = { showDialog = false }) { + Text(stringResource(R.string.simple_cancel)) + } + } + ) + } +} + +@Composable +fun SettingsColorPreference( + title: String, + summary: String? = null, + icon: Any? = null, + initialColor: Int, + onColorSelected: (Int) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + var tempColor by remember { mutableIntStateOf(initialColor) } + + SettingsPreference( + title = title, + summary = summary, + icon = icon, + onClick = { + tempColor = initialColor + showDialog = true + } + ) + + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text(title) }, + text = { + ColorPicker(initialColor = tempColor) { + tempColor = it + } + }, + confirmButton = { + TextButton(onClick = { + onColorSelected(tempColor) + showDialog = false + }) { + Text(stringResource(R.string.simple_ok)) + } + }, + dismissButton = { + TextButton(onClick = { showDialog = false }) { + Text(stringResource(R.string.simple_cancel)) + } + } + ) + } +} + +@Composable +fun SettingsIcon(icon: Any?) { + Box(modifier = Modifier.size(24.dp), contentAlignment = Alignment.Center) { + when (icon) { + is ImageVector -> Icon(icon, contentDescription = null, tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) + is Painter -> Icon(icon, contentDescription = null, tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) + } + } +} + +@Preview(showBackground = true) +@Composable +fun SettingsScreenPreview() { + MaterialTheme { + SettingsScreen( + onBack = {}, + onAccountSettingsClick = {}, + onAboutClick = {}, + onColorSelected = {}, + onNightModeChanged = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt new file mode 100644 index 0000000..19410a1 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt @@ -0,0 +1,469 @@ +package net.helcel.cowspent.android.statistics + +import android.annotation.SuppressLint +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Group +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.res.stringResource +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.* +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.model.DBMember +import androidx.core.graphics.toColorInt + +@OptIn(ExperimentalMaterialApi::class) +@SuppressLint("UseKtx") +@Composable +fun ProjectSankeyDiagram( + projectName: String, + allMembers: List, + allBills: List, + customCategories: List, + onShareReady: (String) -> Unit +) { + val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName) + + var selectedMemberId by remember { mutableLongStateOf(-1L) } + var expanded by remember { mutableStateOf(false) } + + val activeBills = remember(allBills) { + allBills.filter { + it.state != DBBill.STATE_DELETED && + it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT + } + } + + val membersMap = remember(allMembers) { allMembers.associateBy { it.id } } + val categoriesMap = remember(customCategories) { customCategories.associateBy { it.remoteId.toInt() } } + + if (activeBills.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("No data to display", style = MaterialTheme.typography.h6) + } + return + } + + // Consolidated spending calculation + val spendings = remember(activeBills, selectedMemberId, membersMap) { + val spentMap = mutableMapOf() + val catMap = mutableMapOf() + membersMap.keys.forEach { spentMap[it] = 0.0 } + + activeBills.forEach { bill -> + val totalWeight = bill.billOwers.sumOf { bo -> + membersMap[bo.memberId]?.weight ?: 1.0 + } + if (totalWeight > 0) { + if (selectedMemberId == -1L) { + catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + bill.amount + bill.billOwers.forEach { bo -> + val weight = membersMap[bo.memberId]?.weight ?: 1.0 + val share = (bill.amount / totalWeight) * weight + spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + share + } + } else { + bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo -> + val weight = membersMap[bo.memberId]?.weight ?: 1.0 + val share = (bill.amount / totalWeight) * weight + catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + share + spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + share + } + } + } + } + + val memberList = spentMap.toList().filter { it.second > 0 }.sortedByDescending { it.second } + val categoryList = catMap.toList().sortedByDescending { it.second } + memberList to categoryList + } + + val displayMemberSpendings = spendings.first + val displayCategorySpendings = spendings.second + val totalAmount = remember(displayMemberSpendings) { displayMemberSpendings.sumOf { it.second } } + + LaunchedEffect(displayMemberSpendings, displayCategorySpendings, totalAmount, projectName) { + val statsText = StringBuilder() + statsText.append("// ").append(shareStatsIntro.replace("\n", "\n// ")).append("\n\n") + + val middleNode = if (selectedMemberId == -1L) "Total" else "Spent" + + displayMemberSpendings.forEach { (memberId, amount) -> + val name = membersMap[memberId]?.name ?: "???" + statsText.append("$name [${formatShortValue(amount)}] $middleNode\n") + } + + statsText.append("\n") + + displayCategorySpendings.forEach { (catRemoteId, amount) -> + val name = categoriesMap[catRemoteId]?.name ?: "Other" + statsText.append("$middleNode [${formatShortValue(amount)}] $name\n") + } + + statsText.append("\n") + + displayMemberSpendings.forEach { (memberId, _) -> + val member = membersMap[memberId] + if (member != null) { + val hexColor = String.format("#%02x%02x%02x", member.r ?: 128, member.g ?: 128, member.b ?: 128) + statsText.append(":${member.name} $hexColor\n") + } + } + + displayCategorySpendings.forEach { (catRemoteId, _) -> + val category = categoriesMap[catRemoteId] + if (category != null) { + statsText.append(":${category.name ?: "Other"} ${category.color}\n") + } + } + + onShareReady(statsText.toString()) + } + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + val selectedMember = membersMap[selectedMemberId] + EditableExposedDropdownMenu( + value = selectedMember?.name ?: "All Members", + placeholder = "Filter by member", + expanded = expanded, + onExpandedChange = { expanded = it }, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedMember != null) { + UserAvatar( + name = selectedMember.name, + r = selectedMember.r, + g = selectedMember.g, + b = selectedMember.b, + disabled = !selectedMember.isActivated, + size = 24.dp + ) + } else { + Icon(Icons.Default.Group, contentDescription = null) + } + } + }, + content = { + DropdownMenuItem(onClick = { + selectedMemberId = -1L + expanded = false + }) { + Icon(Icons.Default.Group, contentDescription = null) + Spacer(modifier = Modifier.width(12.dp)) + Text("All Members") + } + allMembers.forEach { member -> + DropdownMenuItem(onClick = { + selectedMemberId = member.id + expanded = false + }) { + UserAvatar( + name = member.name, + r = member.r, + g = member.g, + b = member.b, + disabled = !member.isActivated, + size = 24.dp + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(member.name) + } + } + } + ) + + if (totalAmount <= 0) { + Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { + Text("No spending data for selected filter", style = MaterialTheme.typography.body1) + } + } else { + BoxWithConstraints(modifier = Modifier.weight(1f).fillMaxWidth()) { + val nodeHeightMember = 76.dp + val nodeHeightCategory = 76.dp + val nodeHeightTotal = 42.dp + val horizontalGap = 8.dp + + val memberCount = displayMemberSpendings.size + val categoryCount = displayCategorySpendings.size + + val maxGaps = maxOf(memberCount - 1, categoryCount - 1, 0) + val usableWidth = (maxWidth.value - horizontalGap.value * maxGaps).coerceAtLeast(0f) + val moneyScale = if (totalAmount > 0) usableWidth / totalAmount.toFloat() else 0f + + Canvas(modifier = Modifier.fillMaxSize()) { + val nodeHeightMemberPx = nodeHeightMember.toPx() + val nodeHeightCategoryPx = nodeHeightCategory.toPx() + val nodeHeightTotalPx = nodeHeightTotal.toPx() + val gapPx = horizontalGap.toPx() + + val totalWidthPx = size.width + val usableWidthPx = totalWidthPx - gapPx * maxGaps + val moneyScalePx = if (totalAmount > 0) usableWidthPx / totalAmount else 0.0 + + val totalNodeWidthPx = (totalAmount * moneyScalePx).toFloat() + val totalNodeXPx = (totalWidthPx - totalNodeWidthPx) / 2 + + val topY = 0f + val middleY = (size.height - nodeHeightTotalPx) / 2 + val bottomY = size.height - nodeHeightCategoryPx + + val totalNodeColor = Color(0xFF333333) + + // Flows + var currentXTop = (totalWidthPx - (totalAmount * moneyScalePx + (memberCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2 + var currentXInTotalTop = totalNodeXPx + displayMemberSpendings.forEach { (memberId, amount) -> + val member = membersMap[memberId] + val boxWidth = (amount * moneyScalePx).toFloat() + val color = member?.let { + Color(it.r ?: 128, it.g ?: 128, it.b ?: 128) + } ?: Color.Gray + + if (boxWidth > 0.5f) { + drawSankeyFlow( + startX = currentXTop, + startY = topY + nodeHeightMemberPx * 0.5f, + startWidth = boxWidth, + endX = currentXInTotalTop, + endY = middleY + nodeHeightTotalPx * 0.5f, + endWidth = boxWidth, + startColor = color.copy(alpha = 0.5f), + endColor = totalNodeColor.copy(alpha = 0.35f) + ) + } + currentXTop += boxWidth + gapPx + currentXInTotalTop += boxWidth + } + + var currentXBottom = (totalWidthPx - (totalAmount * moneyScalePx + (categoryCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2 + var currentXInTotalBottom = totalNodeXPx + displayCategorySpendings.forEach { (catRemoteId, amount) -> + val category = categoriesMap[catRemoteId] + val boxWidth = (amount * moneyScalePx).toFloat() + val color = category?.color?.let { + try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) } + } ?: Color(0xFF999999) + + if (boxWidth > 0.5f) { + drawSankeyFlow( + startX = currentXInTotalBottom, + startY = middleY + nodeHeightTotalPx * 0.5f, + startWidth = boxWidth, + endX = currentXBottom, + endY = bottomY + nodeHeightCategoryPx * 0.5f, + endWidth = boxWidth, + startColor = totalNodeColor.copy(alpha = 0.35f), + endColor = color.copy(alpha = 0.5f) + ) + } + currentXInTotalBottom += boxWidth + currentXBottom += boxWidth + gapPx + } + + // Nodes + currentXTop = (totalWidthPx - (totalAmount * moneyScalePx + (memberCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2 + displayMemberSpendings.forEach { (memberId, amount) -> + val member = membersMap[memberId] + val width = (amount * moneyScalePx).toFloat() + val color = member?.let { + Color(it.r ?: 128, it.g ?: 128, it.b ?: 128) + } ?: Color.Gray + + if (width > 0.5f) { + drawRoundRect( + color = color, + topLeft = Offset(currentXTop, topY), + size = Size(width, nodeHeightMemberPx), + cornerRadius = CornerRadius(10.dp.toPx()) + ) + } + currentXTop += width + gapPx + } + + drawRect( + color = totalNodeColor, + topLeft = Offset(totalNodeXPx, middleY), + size = Size(totalNodeWidthPx, nodeHeightTotalPx) + ) + + currentXBottom = (totalWidthPx - (totalAmount * moneyScalePx + (categoryCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2 + displayCategorySpendings.forEach { (catRemoteId, amount) -> + val category = categoriesMap[catRemoteId] + val width = (amount * moneyScalePx).toFloat() + val color = category?.color?.let { + try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) } + } ?: Color(0xFF999999) + + if (width > 0.5f) { + drawRoundRect( + color = color, + topLeft = Offset(currentXBottom, bottomY), + size = Size(width, nodeHeightCategoryPx), + cornerRadius = CornerRadius(10.dp.toPx()) + ) + } + currentXBottom += width + gapPx + } + } + + // Member labels + var currentXTopLabel = (maxWidth.value - (totalAmount.toFloat() * moneyScale + (memberCount - 1).coerceAtLeast(0) * horizontalGap.value)) / 2 + displayMemberSpendings.forEach { (memberId, amount) -> + val member = membersMap[memberId] + val widthValue = amount.toFloat() * moneyScale + val width = widthValue.dp + if (widthValue > 12f) { + Box( + modifier = Modifier + .offset(x = currentXTopLabel.dp, y = 0.dp) + .size(width, nodeHeightMember), + contentAlignment = Alignment.Center + ) { + if (width >= 36.dp) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = member?.name ?: "???", + fontSize = 16.sp, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold + ) + Text( + text = formatShortValue(amount), + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.9f) + ) + } + } + } + } + currentXTopLabel += widthValue + horizontalGap.value + } + + // Total label + Box( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .height(nodeHeightTotal), + contentAlignment = Alignment.Center + ) { + Text( + text = if (selectedMemberId == -1L) formatShortValue(totalAmount) else "SPENT: ${formatShortValue(totalAmount)}", + fontSize = 16.sp, + color = Color.White, + textAlign = TextAlign.Center, + fontWeight = FontWeight.ExtraBold + ) + } + + // Category labels + var currentXBottomLabel = (maxWidth.value - (totalAmount.toFloat() * moneyScale + (categoryCount - 1).coerceAtLeast(0) * horizontalGap.value)) / 2 + displayCategorySpendings.forEach { (catRemoteId, amount) -> + val category = categoriesMap[catRemoteId] + val widthValue = amount.toFloat() * moneyScale + val width = widthValue.dp + if (widthValue > 12f) { + Box( + modifier = Modifier + .align(Alignment.BottomStart) + .offset(x = currentXBottomLabel.dp) + .size(width, nodeHeightCategory), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = category?.icon ?: "❔", + fontSize = 20.sp + ) + if (width >= 32.dp) { + Text( + text = formatShortValue(amount), + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.9f) + ) + } + } + } + } + currentXBottomLabel += widthValue + horizontalGap.value + } + } + } + Spacer(Modifier.fillMaxWidth().height(32.dp)) + } +} + +private fun DrawScope.drawSankeyFlow( + startX: Float, + startY: Float, + startWidth: Float, + endX: Float, + endY: Float, + endWidth: Float, + startColor: Color, + endColor: Color +) { + val path = Path().apply { + moveTo(startX, startY) + cubicTo( + startX, startY + (endY - startY) * 0.5f, + endX, endY - (endY - startY) * 0.5f, + endX, endY + ) + lineTo(endX + endWidth, endY) + cubicTo( + endX + endWidth, endY - (endY - startY) * 0.5f, + startX + startWidth, startY + (endY - startY) * 0.5f, + startX + startWidth, startY + ) + close() + } + drawPath( + path = path, + brush = Brush.verticalGradient( + colors = listOf(startColor, endColor), + startY = startY, + endY = endY + ) + ) +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +fun ProjectSankeyDiagramPreview() { + MaterialTheme { + ProjectSankeyDiagram( + projectName = "Test Project", + allMembers = StatisticsMockData.members, + allBills = StatisticsMockData.bills, + customCategories = emptyList(), + onShareReady = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt new file mode 100644 index 0000000..1db969f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt @@ -0,0 +1,436 @@ +package net.helcel.cowspent.android.statistics + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ShowChart +import androidx.compose.material.icons.filled.Timeline +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLocale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.formatShortValue +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBMember +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + +enum class SpendingTimeView(val label: String) { + WEEKLY("Weekly"), + MONTHLY("Monthly"), + YEARLY("Yearly") +} + +@Composable +fun ProjectSpendingGraph( + projectName: String, + allMembers: List, + allBills: List, + onShareReady: (String) -> Unit +) { + val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName) + if (allBills.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("No data to display", style = MaterialTheme.typography.h6) + } + return + } + + var timeView by remember { mutableStateOf(SpendingTimeView.MONTHLY) } + var showTotal by remember { mutableStateOf(false) } + var showMovingAverage by remember { mutableStateOf(true) } + + val sortedBills = remember(allBills) { allBills.sortedBy { it.timestamp } } + val projectMaxTimestamp = sortedBills.last().timestamp + + // Period generation logic + val periods = remember(timeView, sortedBills.first().timestamp, projectMaxTimestamp) { + val cal = Calendar.getInstance() + cal.timeInMillis = sortedBills.first().timestamp * 1000 + cal.set(Calendar.HOUR_OF_DAY, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + + when (timeView) { + SpendingTimeView.WEEKLY -> cal.set(Calendar.DAY_OF_WEEK, cal.firstDayOfWeek) + SpendingTimeView.MONTHLY -> cal.set(Calendar.DAY_OF_MONTH, 1) + SpendingTimeView.YEARLY -> cal.set(Calendar.DAY_OF_YEAR, 1) + } + + val list = mutableListOf() + val endLimit = projectMaxTimestamp * 1000 + while (cal.timeInMillis <= endLimit) { + list.add(cal.timeInMillis / 1000) + when (timeView) { + SpendingTimeView.WEEKLY -> cal.add(Calendar.WEEK_OF_YEAR, 1) + SpendingTimeView.MONTHLY -> cal.add(Calendar.MONTH, 1) + SpendingTimeView.YEARLY -> cal.add(Calendar.YEAR, 1) + } + } + list + } + + // Consolidated spending data calculation + val chartData = remember(allMembers, sortedBills, periods, showTotal) { + val memberSpending = mutableMapOf>() + allMembers.forEach { member -> + val spending = mutableListOf() + for (i in periods.indices) { + val start = periods[i] + val end = if (i + 1 < periods.size) periods[i + 1] else Long.MAX_VALUE + val amount = sortedBills.asSequence() + .filter { it.timestamp in start.. + val totalInPeriod = allMembers.sumOf { memberSpending[it.id]?.get(i) ?: 0.0 } + val currentMetric = if (showTotal) totalInPeriod else totalInPeriod / allMembers.size.coerceAtLeast(1) + lastEma = if (i == 0) currentMetric else alpha * currentMetric + (1 - alpha) * lastEma + lastEma + } + + val maxBar = if (showTotal) { + periods.indices.maxOfOrNull { i -> + allMembers.sumOf { memberSpending[it.id]?.get(i) ?: 0.0 } + } ?: 1.0 + } else { + periods.indices.maxOfOrNull { i -> + allMembers.maxOfOrNull { memberSpending[it.id]?.get(i) ?: 0.0 } ?: 0.0 + } ?: 1.0 + } + val maxVal = maxOf(maxBar, trend.maxOrNull() ?: 0.0).coerceAtLeast(1.0) + + Triple(memberSpending, trend, maxVal) + } + + val memberSpendingByPeriod = chartData.first + val trendLine = chartData.second + val maxSpendingInPeriod = chartData.third + + LaunchedEffect(memberSpendingByPeriod, periods, timeView, projectName) { + val statsText = StringBuilder() + statsText.append(shareStatsIntro).append("\n\n") + statsText.append("Spending Trend (${timeView.label}):\n") + periods.indices.forEach { i -> + val timestamp = periods[i] + val dateStr = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT).format(Date(timestamp * 1000)) + val total = allMembers.sumOf { memberSpendingByPeriod[it.id]?.get(i) ?: 0.0 } + if (total > 0) { + statsText.append("- $dateStr: ${total.toInt()}\n") + } + } + val grandTotal = allMembers.sumOf { m -> memberSpendingByPeriod[m.id]?.sum() ?: 0.0 } + statsText.append("\nTotal: ${grandTotal.toInt()}") + onShareReady(statsText.toString()) + } + + Column(modifier = Modifier.padding(16.dp).fillMaxSize()) { + Card( + elevation = 4.dp, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + BoxWithConstraints(modifier = Modifier.fillMaxSize().padding(12.dp)) { + val totalAvailableWidth = constraints.maxWidth.toFloat() + val totalHeight = constraints.maxHeight.toFloat() + + val yAxisWidth = 44.dp + val xAxisHeight = 32.dp + + val density = LocalDensity.current + val yAxisWidthPx = with(density) { yAxisWidth.toPx() } + val xAxisHeightPx = with(density) { xAxisHeight.toPx() } + + val chartHeight = totalHeight - xAxisHeightPx + + // Y-Axis Labels + Box(modifier = Modifier.width(yAxisWidth).height(with(density) { chartHeight.toDp() })) { + for (i in 0..5) { + val value = (i.toFloat() / 5) * maxSpendingInPeriod + val yOffset = chartHeight - (i.toFloat() / 5) * chartHeight + Text( + text = formatShortValue(value), + fontSize = 10.sp, + color = Color.Gray, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .offset(y = with(density) { (yOffset - 6.sp.toPx()).toDp() }) + .padding(end = 8.dp) + ) + } + } + + val scrollState = rememberScrollState() + LaunchedEffect(scrollState.maxValue) { + if (scrollState.maxValue > 0) { + scrollState.scrollTo(scrollState.maxValue) + } + } + val minPeriodWidth = when(timeView) { + SpendingTimeView.WEEKLY -> 48.dp + SpendingTimeView.MONTHLY -> 72.dp + SpendingTimeView.YEARLY -> 110.dp + } + val contentWidth = max(totalAvailableWidth - yAxisWidthPx, with(density) { periods.size * minPeriodWidth.toPx() }) + + Box( + modifier = Modifier + .offset(x = yAxisWidth) + .width(with(density) { (totalAvailableWidth - yAxisWidthPx).toDp() }) + .horizontalScroll(scrollState) + ) { + Column(modifier = Modifier.width(with(density) { contentWidth.toDp() })) { + // Chart area + val primaryColor = MaterialTheme.colors.primary + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(with(density) { chartHeight.toDp() }) + ) { + val gridColor = Color.Gray.copy(alpha = 0.15f) + for (i in 0..5) { + val y = size.height - (i.toFloat() / 5) * size.height + drawLine( + color = gridColor, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 1.dp.toPx() + ) + } + + val fullWidthPerPeriod = size.width / periods.size + val groupWidth = fullWidthPerPeriod * 0.75f + val groupSpacing = fullWidthPerPeriod * 0.25f + + periods.indices.forEach { i -> + if (showTotal) { + val totalAmount = allMembers.sumOf { memberSpendingByPeriod[it.id]?.get(i) ?: 0.0 } + if (totalAmount > 0) { + val barHeight = (totalAmount.toFloat() / maxSpendingInPeriod.toFloat()) * size.height + val x = i * fullWidthPerPeriod + groupSpacing / 2 + drawRoundRect( + color = primaryColor.copy(alpha = 0.8f), + topLeft = Offset(x, size.height - barHeight), + size = Size(groupWidth, barHeight), + cornerRadius = CornerRadius(4.dp.toPx(), 4.dp.toPx()) + ) + } + } else { + val activeMembers = allMembers.filter { (memberSpendingByPeriod[it.id]?.get(i) ?: 0.0) > 0 } + if (activeMembers.isNotEmpty()) { + val memberBarWidth = groupWidth / allMembers.size + val groupStartX = i * fullWidthPerPeriod + groupSpacing / 2 + val totalActiveWidth = activeMembers.size * memberBarWidth + val centeringOffset = (groupWidth - totalActiveWidth) / 2 + + activeMembers.forEachIndexed { activeIndex, member -> + val amount = memberSpendingByPeriod[member.id]!![i] + val barHeight = (amount.toFloat() / maxSpendingInPeriod.toFloat()) * size.height + val x = groupStartX + centeringOffset + activeIndex * memberBarWidth + + drawRoundRect( + color = Color(android.graphics.Color.rgb(member.r ?: 0, member.g ?: 0, member.b ?: 0)), + topLeft = Offset(x, size.height - barHeight), + size = Size(memberBarWidth * 0.85f, barHeight), + cornerRadius = CornerRadius(2.dp.toPx(), 2.dp.toPx()) + ) + } + } + } + } + + if (showMovingAverage && periods.isNotEmpty()) { + val linePath = Path() + trendLine.forEachIndexed { i, value -> + val x = i * fullWidthPerPeriod + fullWidthPerPeriod / 2f + val y = size.height - (value.toFloat() / maxSpendingInPeriod.toFloat()) * size.height + + if (i == 0) { + linePath.moveTo(x, y) + } else { + val prevX = (i - 1) * fullWidthPerPeriod + fullWidthPerPeriod / 2f + val prevY = size.height - (trendLine[i-1].toFloat() / maxSpendingInPeriod.toFloat()) * size.height + linePath.cubicTo( + prevX + (x - prevX) / 2f, prevY, + prevX + (x - prevX) / 2f, y, + x, y + ) + } + } + drawPath( + path = linePath, + color = primaryColor, + style = Stroke( + width = 2.5.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + ) + } + } + + Box(modifier = Modifier.fillMaxWidth().height(xAxisHeight)) { + val locale = LocalLocale.current.platformLocale + val labelIndices = when(timeView) { + SpendingTimeView.YEARLY -> periods.indices.toList() + SpendingTimeView.MONTHLY -> periods.indices.filter { it % 2 == 0 || it == periods.size - 1 } + SpendingTimeView.WEEKLY -> periods.indices.filter { it % 4 == 0 || it == periods.size - 1 } + } + + labelIndices.forEach { index -> + val timestamp = periods[index] + val cal = Calendar.getInstance().apply { timeInMillis = timestamp * 1000 } + val dateStr = when (timeView) { + SpendingTimeView.WEEKLY -> { + val week = cal.get(Calendar.WEEK_OF_YEAR) + val year = cal.get(Calendar.YEAR) % 100 + String.format(Locale.ROOT, "%02d/%02d", week, year) + } + SpendingTimeView.MONTHLY -> SimpleDateFormat("MM/yy", locale).format(Date(timestamp * 1000)) + SpendingTimeView.YEARLY -> "${cal.get(Calendar.YEAR)}" + } + val fullWidthPerPeriod = contentWidth / periods.size + val xPosition = index * fullWidthPerPeriod + (fullWidthPerPeriod / 2) + Text( + text = dateStr, + fontSize = 10.sp, + color = Color.Gray, + textAlign = TextAlign.Center, + modifier = Modifier + .offset(x = with(density) { (xPosition - 40.dp.toPx()).toDp() }) + .width(80.dp) + ) + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .background(Color.Black.copy(alpha = 0.05f), MaterialTheme.shapes.medium) + .border(1.dp, Color.Black.copy(alpha = 0.1f), MaterialTheme.shapes.medium) + ) { + Row(modifier = Modifier.padding(4.dp)) { + SpendingTimeView.entries.forEach { view -> + val isSelected = timeView == view + Box( + modifier = Modifier + .background( + color = if (isSelected) MaterialTheme.colors.primary else Color.Transparent, + shape = MaterialTheme.shapes.medium + ) + .clickable { timeView = view } + .padding(horizontal = 12.dp, vertical = 6.dp) + ) { + Text( + text = view.label, + fontSize = 12.sp, + color = if (isSelected) MaterialTheme.colors.onPrimary else Color.Gray, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal + ) + } + } + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { showMovingAverage = !showMovingAverage }) { + Icon( + if (showMovingAverage) Icons.Default.Timeline else Icons.AutoMirrored.Filled.ShowChart, + contentDescription = "Trend", + tint = if (showMovingAverage) MaterialTheme.colors.primary else Color.Gray + ) + } + Spacer(Modifier.width(4.dp)) + Text("Total", style = MaterialTheme.typography.caption, color = Color.Gray) + Switch( + checked = showTotal, + onCheckedChange = { showTotal = it }, + colors = SwitchDefaults.colors(checkedThumbColor = MaterialTheme.colors.primary) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (!showTotal) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 100.dp) + .verticalScroll(scrollState) + ) { + allMembers.chunked(3).forEach { rowMembers -> + Row(modifier = Modifier.fillMaxWidth()) { + rowMembers.forEach { member -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f).padding(bottom = 8.dp) + ) { + Box( + modifier = Modifier + .size(12.dp) + .background( + Color(android.graphics.Color.rgb(member.r ?: 0, member.g ?: 0, member.b ?: 0)), + shape = MaterialTheme.shapes.small + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(member.name, fontSize = 12.sp, maxLines = 1) + } + } + if (rowMembers.size == 1) Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } +} + + +@Preview(showBackground = true) +@Composable +fun ProjectSpendingGraphPreview() { + MaterialTheme { + ProjectSpendingGraph( + projectName = "Test Project", + allMembers = StatisticsMockData.members, + allBills = StatisticsMockData.bills, + onShareReady = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt new file mode 100644 index 0000000..8ff5c6a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt @@ -0,0 +1,56 @@ +package net.helcel.cowspent.android.statistics + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.theme.ThemeUtils + +class ProjectStatisticsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, -1L) + if (projectId == -1L) { + finish() + return + } + + val db = CowspentSQLiteOpenHelper.getInstance(this) + + lifecycleScope.launch { + val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } + if (proj == null) { + finish() + return@launch + } + + setContent { + ThemeUtils.CowspentTheme { + ProjectStatisticsScreen( + proj = proj, + db = db, + onBack = { finish() } + ) + } + } + } + } + + companion object { + private const val EXTRA_PROJECT_ID = "extra_project_id" + + fun createIntent(context: Context, projectId: Long): Intent { + return Intent(context, ProjectStatisticsActivity::class.java).apply { + putExtra(EXTRA_PROJECT_ID, projectId) + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsPreview.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsPreview.kt new file mode 100644 index 0000000..04a4257 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsPreview.kt @@ -0,0 +1,48 @@ +package net.helcel.cowspent.android.statistics + +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBBillOwer +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType + +object StatisticsMockData { + val project = DBProject( + id = 1L, + remoteId = "vacation", + password = "", + name = "Vacation 2024", + serverUrl = null, + email = null, + lastPayerId = 1L, + type = ProjectType.LOCAL, + lastSyncedTimestamp = null, + currencyName = "EUR", + isDeletionDisabled = false, + myAccessLevel = 4, + bearerToken = null, + archivedTs = null + ) + + val members = listOf( + DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null), + DBMember(2L, 0, 1L, "Bob", true, 1.0, 0, 100, 255, 100, null, null), + DBMember(3L, 0, 1L, "Charlie", true, 1.0, 0, 100, 100, 255, null, null) + ) + + val bills = listOf( + DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000 - 86400 * 10, "Hotel", 0, null, null, -13, "", -1).apply { + billOwers = listOf(DBBillOwer(1L, 1L, 1L), DBBillOwer(2L, 1L, 2L), DBBillOwer(3L, 1L, 3L)) + }, + DBBill(2L, 0, 1L, 2L, 50.0, System.currentTimeMillis() / 1000 - 86400 * 8, "Groceries", 0, null, null, -1, "", -2).apply { + billOwers = listOf(DBBillOwer(4L, 2L, 1L), DBBillOwer(5L, 2L, 2L), DBBillOwer(6L, 2L, 3L)) + }, + DBBill(3L, 0, 1L, 3L, 1.0, System.currentTimeMillis() / 1000 - 86400 * 5, "Gas", 0, null, null, -14, "", -1).apply { + billOwers = listOf(DBBillOwer(7L, 3L, 1L), DBBillOwer(8L, 3L, 2L), DBBillOwer(9L, 3L, 3L)) + }, + DBBill(4L, 0, 1L, 1L, 30.0, System.currentTimeMillis() / 1000 - 86400 * 2, "Dinner", 0, null, null, -12, "", -2).apply { + billOwers = listOf(DBBillOwer(10L, 4L, 1L), DBBillOwer(11L, 4L, 2L)) + } + ) +} + diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt new file mode 100644 index 0000000..7e19785 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt @@ -0,0 +1,155 @@ +package net.helcel.cowspent.android.statistics + +import android.content.Intent +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.Alignment +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.CategoryUtils + +@Composable +fun ProjectStatisticsScreen( + proj: DBProject, + db: CowspentSQLiteOpenHelper, + onBack: () -> Unit +) { + val context = LocalContext.current + val prefs = remember { PreferenceManager.getDefaultSharedPreferences(context) } + var selectedTab by rememberSaveable { + mutableIntStateOf(prefs.getInt("last_statistics_tab", 0)) + } + var currentShareText by remember { mutableStateOf("") } + val tabs = listOf( + stringResource(R.string.statistic_title), + "Trend", + "Sankey" + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.statistic_title)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + IconButton(onClick = { + if (currentShareText.isNotEmpty()) { + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, currentShareText) + type = "text/plain" + } + context.startActivity(Intent.createChooser(sendIntent, null)) + } + }) { + Icon(Icons.Default.Share, contentDescription = "Share") + } + }, + backgroundColor = MaterialTheme.colors.primary, + elevation = 0.dp + ) + } + ) { padding -> + Column(modifier = Modifier.padding(padding).fillMaxSize()) { + TabRow(selectedTabIndex = selectedTab) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTab == index, + onClick = { + selectedTab = index + prefs.edit { putInt("last_statistics_tab", index) } + }, + text = { Text(title) } + ) + } + } + + val statsData by produceState(null, proj.id) { + value = withContext(Dispatchers.IO) { + val members = db.getMembersOfProject(proj.id, null) + val bills = db.getBillsOfProject(proj.id) + val categories = db.getCategories(proj.id) + val paymentModes = db.getPaymentModes(proj.id) + StatisticsData(members, bills, categories, paymentModes) + } + } + + if (statsData != null) { + val data = statsData!! + val defaultCategories = remember(proj.id) { CategoryUtils.getDefaultCategories(context, proj.id) } + val categories = remember(proj.type, data.categories, defaultCategories) { + val hardcoded = if (proj.type == ProjectType.LOCAL) { + defaultCategories + } else { + listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT }) + } + (data.categories + hardcoded).distinctBy { it.remoteId } + } + val categoryNoneLabel = stringResource(R.string.category_none) + val sankeyCategories = remember(proj.id, data.categories, defaultCategories, categoryNoneLabel) { + val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "❌", "#9E9E9E") + (data.categories + defaultCategories + noneCategory).distinctBy { it.remoteId } + } + + when (selectedTab) { + 0 -> { + ProjectStatisticsTable( + proj = proj, + allMembers = data.members, + allBills = data.bills, + customCategories = categories, + customPaymentModes = data.paymentModes, + onShareReady = { currentShareText = it } + ) + } + 1 -> { + ProjectSpendingGraph( + projectName = proj.name.ifEmpty { proj.remoteId }, + allMembers = data.members, + allBills = data.bills, + onShareReady = { currentShareText = it } + ) + } + 2 -> { + ProjectSankeyDiagram( + projectName = proj.name.ifEmpty { proj.remoteId }, + allMembers = data.members, + allBills = data.bills, + customCategories = sankeyCategories, + onShareReady = { currentShareText = it } + ) + } + } + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + } + } +} + +data class StatisticsData( + val members: List, + val bills: List, + val categories: List, + val paymentModes: List +) diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt new file mode 100644 index 0000000..b157c1e --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt @@ -0,0 +1,390 @@ +package net.helcel.cowspent.android.statistics + +import android.app.DatePickerDialog +import android.content.Context +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.* +import net.helcel.cowspent.model.* +import net.helcel.cowspent.util.CategoryUtils +import net.helcel.cowspent.util.SupportUtil +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.abs +import kotlin.math.round + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ProjectStatisticsTable( + proj: DBProject, + allMembers: List, + allBills: List, + customCategories: List, + customPaymentModes: List, + onShareReady: (String) -> Unit +) { + val context = LocalContext.current + val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) } + val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) } + + var categoryId by remember { mutableIntStateOf(-1000) } + var paymentModeId by remember { mutableIntStateOf(-1000) } + var dateMin by remember { mutableStateOf(null) } + var dateMax by remember { mutableStateOf(null) } + + val categoryAll = stringResource(R.string.category_all) + val categoryNone = stringResource(R.string.category_none) + val categoryReimbursement = stringResource(R.string.category_reimbursement) + val categoryAllExceptReimbursement = stringResource(R.string.category_all_except_reimbursement) + + val paymentModeAll = stringResource(R.string.payment_mode_all) + val paymentModeNone = stringResource(R.string.payment_mode_none) + + val shareStatsHeader = stringResource(R.string.share_stats_header) + val shareStatsIntro = stringResource(R.string.share_stats_intro, proj.name.ifEmpty { proj.remoteId }) + + val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) { + val list = mutableListOf>() + list.add(Triple(-1000, "📋", categoryAll)) + list.add(Triple(-100, "🧾", categoryAllExceptReimbursement)) + list.add(Triple(0, "❌", categoryNone)) + + val catsToUse = if (proj.type == ProjectType.LOCAL) { + CategoryUtils.getDefaultCategories(context, proj.id) + } else { + customCategories.ifEmpty { + CategoryUtils.getDefaultCategories(context, proj.id) + } + } + + catsToUse.forEach { + list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) + } + list.distinctBy { it.first } + } + + val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) { + val list = mutableListOf>() + list.add(Triple(-1000, "💳", paymentModeAll)) + list.add(Triple(0, "❌", paymentModeNone)) + + val pmsToUse = if (proj.type == ProjectType.LOCAL) { + CategoryUtils.getDefaultPaymentModes(context, proj.id) + } else { + customPaymentModes.ifEmpty { + CategoryUtils.getDefaultPaymentModes(context, proj.id) + } + } + + pmsToUse.forEach { + list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) + } + list.distinctBy { it.first } + } + + val stats = remember(allMembers, allBills, categoryId, paymentModeId, dateMin, dateMax, shareStatsHeader, shareStatsIntro) { + val membersNbBills = mutableMapOf() + val membersBalance = HashMap() + val membersPaid = HashMap() + val membersSpent = HashMap() + + SupportUtil.getStats( + allMembers, allBills, + membersNbBills, membersBalance, membersPaid, membersSpent, + categoryId, paymentModeId, dateMin, dateMax + ) + + var statsText = shareStatsIntro + "\n\n" + statsText += shareStatsHeader + "\n" + + var totalPaid = 0.0 + val memberStats = allMembers.map { m -> + val mPaid = membersPaid[m.id] ?: 0.0 + totalPaid += mPaid + val mSpent = membersSpent[m.id] ?: 0.0 + val mBalance = membersBalance[m.id] ?: 0.0 + + val rpaid = round(mPaid * 100.0) / 100.0 + val rspent = round(mSpent * 100.0) / 100.0 + val rbalance = round(abs(mBalance) * 100.0) / 100.0 + val sign = if (mBalance > 0.01) "+" else if (mBalance < -0.01) "-" else "" + + statsText += "\n${m.name} (" + statsText += (if (rpaid == 0.0) "--" else SupportUtil.normalNumberFormat.format(rpaid)) + " | " + statsText += (if (rspent == 0.0) "--" else SupportUtil.normalNumberFormat.format(rspent)) + " | " + statsText += "$sign${SupportUtil.normalNumberFormat.format(rbalance)})" + + MemberStat(m.name, mPaid, mSpent, mBalance) + } + + StatsResult(memberStats, totalPaid, statsText) + } + + val dateMinLong = remember(dateMin) { + dateMin?.let { try { sdf.parse(it)?.time } catch (_: Exception) { null } } + } + val dateMaxLong = remember(dateMax) { + dateMax?.let { try { sdf.parse(it)?.time } catch (_: Exception) { null } } + } + + LaunchedEffect(stats.statsText) { + onShareReady(stats.statsText) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Card( + elevation = 2.dp, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(12.dp)) { + var categoryExpanded by remember { mutableStateOf(value = false) } + val selectedCategory = categories.find { it.first == categoryId } + + EditableExposedDropdownMenu( + value = selectedCategory?.third ?: "", + placeholder = stringResource(R.string.setting_category), + expanded = categoryExpanded, + onExpandedChange = { categoryExpanded = it }, + onDismissRequest = { categoryExpanded = false }, + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedCategory != null) { + Text(text = selectedCategory.second, fontSize = 20.sp) + } else { + Icon(Icons.Default.Category, contentDescription = null) + } + } + }, + content = { + categories.forEach { category -> + DropdownMenuItem(onClick = { + categoryId = category.first + categoryExpanded = false + }) { + Text(text = category.second, fontSize = 20.sp) + Spacer(modifier = Modifier.width(12.dp)) + Text(category.third) + } + } + } + ) + + Spacer(modifier = Modifier.height(12.dp)) + + var pmExpanded by remember { mutableStateOf(false) } + val selectedPm = paymentModes.find { it.first == paymentModeId } + + EditableExposedDropdownMenu( + value = selectedPm?.third ?: "", + placeholder = stringResource(R.string.setting_payment_mode), + expanded = pmExpanded, + onExpandedChange = { pmExpanded = it }, + onDismissRequest = { pmExpanded = false }, + leadingIcon = { + Box(modifier = Modifier.padding(start = 12.dp)) { + if (selectedPm != null) { + Text(text = selectedPm.second, fontSize = 20.sp) + } else { + Icon(Icons.Default.Payment, contentDescription = null) + } + } + }, + content = { + paymentModes.forEach { pm -> + DropdownMenuItem(onClick = { + paymentModeId = pm.first + pmExpanded = false + }) { + Text(text = pm.second, fontSize = 20.sp) + Spacer(modifier = Modifier.width(12.dp)) + Text(pm.third) + } + } + } + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + ClickableOutlinedTextField( + value = dateMin?.let { sdf.parse(it)?.let { d -> dateFormat.format(d) } } ?: "", + onClick = { + showDatePicker(context, dateMin, sdf, maxDate = dateMaxLong) { dateMin = it } + }, + modifier = Modifier.weight(1f), + placeholder = { Text(stringResource(R.string.stats_date_min)) }, + leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }, + trailingIcon = if (dateMin != null) { + { + IconButton(onClick = { dateMin = null }) { + Icon(Icons.Default.Close, contentDescription = null, tint = Color.Red.copy(alpha = 0.6f)) + } + } + } else null + ) + + Spacer(modifier = Modifier.width(8.dp)) + + ClickableOutlinedTextField( + value = dateMax?.let { sdf.parse(it)?.let { d -> dateFormat.format(d) } } ?: "", + onClick = { + showDatePicker(context, dateMax, sdf, minDate = dateMinLong) { dateMax = it } + }, + modifier = Modifier.weight(1f), + placeholder = { Text(stringResource(R.string.stats_date_max)) }, + leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }, + trailingIcon = if (dateMax != null) { + { + IconButton(onClick = { dateMax = null }) { + Icon(Icons.Default.Close, contentDescription = null, tint = Color.Red.copy(alpha = 0.6f)) + } + } + } else null + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Table Header + Surface( + color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f), + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth() + ) { + Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp)) { + Text(stringResource(R.string.stats_who), modifier = Modifier.weight(2f), fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp) + Text(stringResource(R.string.stats_paid), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp) + Text(stringResource(R.string.stats_spent), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp) + Text(stringResource(R.string.stats_balance), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp) + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + LazyColumn(modifier = Modifier.weight(1f)) { + items(stats.memberStats) { m -> + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) { + Text(m.name, modifier = Modifier.weight(2f), color = MaterialTheme.colors.onSurface, fontWeight = FontWeight.Medium) + + Text( + if (m.paid == 0.0) "--" else SupportUtil.normalNumberFormat.format(m.paid), + modifier = Modifier.weight(1.5f), + textAlign = TextAlign.End, + color = MaterialTheme.colors.onSurface + ) + + Text( + if (m.spent == 0.0) "--" else SupportUtil.normalNumberFormat.format(m.spent), + modifier = Modifier.weight(1.5f), + textAlign = TextAlign.End, + color = MaterialTheme.colors.onSurface + ) + + val balanceColor = if (m.balance > 0.01) colorResource(R.color.green) else if (m.balance < -0.01) colorResource(R.color.red) else MaterialTheme.colors.onSurface + val sign = if (m.balance > 0.01) "+" else if (m.balance < -0.01) "-" else "" + Text( + "$sign${SupportUtil.normalNumberFormat.format(abs(m.balance))}", + modifier = Modifier.weight(1.5f), + textAlign = TextAlign.End, + color = balanceColor, + fontWeight = FontWeight.Bold + ) + } + Divider(thickness = 0.5.dp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)) + } + } + + + Card( + elevation = 4.dp, + shape = MaterialTheme.shapes.medium, + backgroundColor = MaterialTheme.colors.primary, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier.padding(16.dp, 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.total, SupportUtil.normalNumberFormat.format(stats.totalPaid)), + style = MaterialTheme.typography.h6, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.onPrimary + ) + } + } + } +} + +private fun showDatePicker( + context: Context, + currentDate: String?, + sdf: SimpleDateFormat, + minDate: Long? = null, + maxDate: Long? = null, + onDateSelected: (String) -> Unit +) { + val calendar = Calendar.getInstance() + currentDate?.let { + try { sdf.parse(it)?.let { date -> calendar.time = date } } catch (_: Exception) {} + } + val dialog = DatePickerDialog( + context, + { _, year, month, day -> + val cal = Calendar.getInstance().apply { + set(Calendar.YEAR, year) + set(Calendar.MONTH, month) + set(Calendar.DAY_OF_MONTH, day) + } + onDateSelected(sdf.format(cal.time)) + }, + calendar[Calendar.YEAR], + calendar[Calendar.MONTH], + calendar[Calendar.DAY_OF_MONTH] + ) + minDate?.let { dialog.datePicker.minDate = it } + maxDate?.let { dialog.datePicker.maxDate = it } + dialog.show() +} + +data class MemberStat(val name: String, val paid: Double, val spent: Double, val balance: Double) +data class StatsResult(val memberStats: List, val totalPaid: Double, val statsText: String) + + +@Preview(showBackground = true) +@Composable +fun ProjectStatisticsTablePreview() { + MaterialTheme { + ProjectStatisticsTable( + proj = StatisticsMockData.project, + allMembers = StatisticsMockData.members, + allBills = StatisticsMockData.bills, + customCategories = emptyList(), + customPaymentModes = emptyList(), + onShareReady = {} + ) } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/Category.kt b/app/src/main/java/net/helcel/cowspent/model/Category.kt new file mode 100644 index 0000000..7ff44e0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/Category.kt @@ -0,0 +1,5 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class Category(val memberName: String?, val memberId: Long?) : Serializable diff --git a/app/src/main/java/net/helcel/cowspent/model/CreditDebt.kt b/app/src/main/java/net/helcel/cowspent/model/CreditDebt.kt new file mode 100644 index 0000000..65c6790 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/CreditDebt.kt @@ -0,0 +1,3 @@ +package net.helcel.cowspent.model + +class CreditDebt(var memberId: Long, var balance: Double) diff --git a/app/src/main/java/net/helcel/cowspent/model/DBAccountProject.kt b/app/src/main/java/net/helcel/cowspent/model/DBAccountProject.kt new file mode 100644 index 0000000..b53ccd0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBAccountProject.kt @@ -0,0 +1,17 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBAccountProject( + var id: Long, + var remoteId: String, + var password: String?, + var name: String, + var ncUrl: String, + var archivedTs: Long? = null +) : Serializable { + + override fun toString(): String { + return "#DBAccountProject$id/$remoteId,$name, $ncUrl, $password, archivedTs=$archivedTs" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBBill.kt b/app/src/main/java/net/helcel/cowspent/model/DBBill.kt new file mode 100644 index 0000000..2ca7d20 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBBill.kt @@ -0,0 +1,121 @@ +package net.helcel.cowspent.model + +import android.util.Log +import java.io.Serializable +import java.util.Calendar +import java.util.Locale + +open class DBBill( + var id: Long, + var remoteId: Long, + var projectId: Long, + var payerId: Long, + var amount: Double, + var timestamp: Long, + var what: String, + var state: Int, + var repeat: String?, + var paymentMode: String?, + var categoryRemoteId: Int, + var comment: String?, + var paymentModeRemoteId: Int +) : Item, Serializable { + + var formattedWhat: String = "" + var formattedSubtitle: String = "" + + var billOwers: List = ArrayList() + + val billOwersIds: List + get() { + val result: MutableList = ArrayList() + for (bo in billOwers) { + result.add(bo.memberId) + } + return result + } + + val date: String + get() { + val cal = Calendar.getInstance() + cal.timeInMillis = timestamp * 1000 + Log.v("ll", "[$what] get date ts $timestamp year ${cal[Calendar.YEAR]}") + val month = cal[Calendar.MONTH] + 1 + val day = cal[Calendar.DAY_OF_MONTH] + return "${cal[Calendar.YEAR]}-${String.format(Locale.ROOT, "%02d", month)}-${ + String.format(Locale.ROOT, "%02d", day) + }" + } + + val time: String + get() { + val cal = Calendar.getInstance() + cal.timeInMillis = timestamp * 1000 + return "${String.format(Locale.ROOT, "%02d", cal[Calendar.HOUR_OF_DAY])}:${ + String.format(Locale.ROOT, "%02d", cal[Calendar.MINUTE]) + }" + } + + + override fun toString(): String { + return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryRemoteId" + } + + override fun isSection(): Boolean { + return false + } + + companion object { + const val PAYMODE_NONE = "n" + const val PAYMODE_CARD = "c" + const val PAYMODE_CASH = "b" + const val PAYMODE_CHECK = "f" + const val PAYMODE_TRANSFER = "t" + const val PAYMODE_ONLINE_SERVICE = "o" + + const val PAYMODE_ID_NONE = 0 + const val PAYMODE_ID_CARD = -1 + const val PAYMODE_ID_CASH = -2 + const val PAYMODE_ID_CHECK = -3 + const val PAYMODE_ID_TRANSFER = -4 + const val PAYMODE_ID_ONLINE_SERVICE = -5 + + @JvmField + val oldPmIdToNew: Map = object : HashMap() { + init { + put(PAYMODE_NONE, PAYMODE_ID_NONE) + put(PAYMODE_CARD, PAYMODE_ID_CARD) + put(PAYMODE_CASH, PAYMODE_ID_CASH) + put(PAYMODE_CHECK, PAYMODE_ID_CHECK) + put(PAYMODE_TRANSFER, PAYMODE_ID_TRANSFER) + put(PAYMODE_ONLINE_SERVICE, PAYMODE_ID_ONLINE_SERVICE) + } + } + + const val CATEGORY_NONE = 0 + const val CATEGORY_GROCERIES = -1 + const val CATEGORY_LEISURE = -2 + const val CATEGORY_RENT = -3 + const val CATEGORY_BILLS = -4 + const val CATEGORY_CULTURE = -5 + const val CATEGORY_HEALTH = -6 + const val CATEGORY_SHOPPING = -10 + const val CATEGORY_REIMBURSEMENT = -11 + const val CATEGORY_RESTAURANT = -12 + const val CATEGORY_ACCOMMODATION = -13 + const val CATEGORY_TRANSPORT = -14 + const val CATEGORY_SPORT = -15 + + const val STATE_OK = 0 + const val STATE_ADDED = 1 + const val STATE_EDITED = 2 + const val STATE_DELETED = 3 + + const val NON_REPEATED = "n" + const val REPEAT_DAY = "d" + const val REPEAT_WEEK = "w" + const val REPEAT_FORTNIGHT = "b" + const val REPEAT_MONTH = "m" + const val REPEAT_YEAR = "y" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBBillOwer.kt b/app/src/main/java/net/helcel/cowspent/model/DBBillOwer.kt new file mode 100644 index 0000000..f281ef6 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBBillOwer.kt @@ -0,0 +1,14 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBBillOwer( + var id: Long, + var billId: Long, + var memberId: Long +) : Serializable { + + override fun toString(): String { + return "#DBBillOwer$id/$billId,$memberId" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBCategory.kt b/app/src/main/java/net/helcel/cowspent/model/DBCategory.kt new file mode 100644 index 0000000..ff71c93 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBCategory.kt @@ -0,0 +1,17 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBCategory( + var id: Long, + var remoteId: Long, + var projectId: Long, + var name: String?, + var icon: String, + var color: String +) : Serializable { + + override fun toString(): String { + return "#DBCategory$id/$remoteId,$name" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBCurrency.kt b/app/src/main/java/net/helcel/cowspent/model/DBCurrency.kt new file mode 100644 index 0000000..9784dec --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBCurrency.kt @@ -0,0 +1,17 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBCurrency( + var id: Long, + var remoteId: Long, + var projectId: Long, + var name: String?, + var exchangeRate: Double, + var state: Int +) : Serializable { + + override fun toString(): String { + return "#DBCurrency$id/$remoteId,$name , state: $state" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBMember.kt b/app/src/main/java/net/helcel/cowspent/model/DBMember.kt new file mode 100644 index 0000000..9eef451 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBMember.kt @@ -0,0 +1,23 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBMember( + var id: Long, + var remoteId: Long, + var projectId: Long, + var name: String, + var isActivated: Boolean, + var weight: Double, + var state: Int, + var r: Int?, + var g: Int?, + var b: Int?, + var ncUserId: String?, + var avatar: String? +) : Serializable { + + override fun toString(): String { + return "#DBMember$id/$remoteId,$name, p$projectId, $weight, $isActivated" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBPaymentMode.kt b/app/src/main/java/net/helcel/cowspent/model/DBPaymentMode.kt new file mode 100644 index 0000000..a35345d --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBPaymentMode.kt @@ -0,0 +1,17 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBPaymentMode( + var id: Long, + var remoteId: Long, + var projectId: Long, + var name: String?, + var icon: String, + var color: String +) : Serializable { + + override fun toString(): String { + return "#DBPaymentMode$id/$remoteId,$name" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/DBProject.kt b/app/src/main/java/net/helcel/cowspent/model/DBProject.kt new file mode 100644 index 0000000..19d513c --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/DBProject.kt @@ -0,0 +1,75 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class DBProject( + var id: Long, + var remoteId: String, + var password: String, + var name: String, + var serverUrl: String?, + var email: String?, + var lastPayerId: Long?, + var type: ProjectType, + var lastSyncedTimestamp: Long?, + var currencyName: String?, + var isDeletionDisabled: Boolean, + var myAccessLevel: Int, + var bearerToken: String?, + var archivedTs: Long? = null, + var latestBillTs: Long = 0L +) : Serializable { + + val isArchived: Boolean + get() = archivedTs != null && archivedTs!! > 0 + + val isLocal: Boolean + get() = ProjectType.LOCAL == type + + fun getRequestBaseUrl(isOcsRequest: Boolean): String { + val url = serverUrl ?: "" + return if (!isOcsRequest) { + url.replace("/+$".toRegex(), "") + } else { + url.replace("/+$".toRegex(), "") + .replace("/index.php/apps/cospend", "/ocs/v2.php/apps/cospend") + } + } + + override fun toString(): String { + return "#DBProject$id/$remoteId,$name, $serverUrl, $email" + } + + fun isShareable(): Boolean { + return !serverUrl.isNullOrEmpty() + } + + fun getShareUrl(): String { + val url = serverUrl ?: "" + val strippedUrl = url + .replace("https://", "") + .replace("http://", "") + .replace("/index.php/apps/cospend", "") + + val protocol = if (type == ProjectType.IHATEMONEY) "ihatemoney" else "cospend" + return "$protocol://$strippedUrl/$remoteId/$password" + } + + fun getPublicWebUrl(): String { + val url = serverUrl ?: "" + return if (url.contains("index.php/apps/cospend")) { + "$url/loginproject/$remoteId" + } else { + "$url/$remoteId" + } + } + + companion object { + const val ACCESS_LEVEL_UNKNOWN = -1 + const val ACCESS_LEVEL_NONE = 0 + const val ACCESS_LEVEL_VIEWER = 1 + const val ACCESS_LEVEL_PARTICIPANT = 2 + const val ACCESS_LEVEL_MAINTAINER = 3 + const val ACCESS_LEVEL_ADMIN = 4 + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/GroupedBill.kt b/app/src/main/java/net/helcel/cowspent/model/GroupedBill.kt new file mode 100644 index 0000000..51947a4 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/GroupedBill.kt @@ -0,0 +1,26 @@ +package net.helcel.cowspent.model + +import java.io.Serializable + +class GroupedBill( + val sourceBills: List +) : DBBill( + sourceBills.first().id, + sourceBills.first().remoteId, + sourceBills.first().projectId, + sourceBills.first().payerId, + sourceBills.sumOf { it.amount }, + sourceBills.first().timestamp, + sourceBills.first().what, + sourceBills.first().state, + sourceBills.first().repeat, + sourceBills.first().paymentMode, + sourceBills.first().categoryRemoteId, + sourceBills.first().comment, + sourceBills.first().paymentModeRemoteId +), Serializable { + init { + this.formattedWhat = sourceBills.first().formattedWhat + this.billOwers = sourceBills.flatMap { it.billOwers } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/Item.kt b/app/src/main/java/net/helcel/cowspent/model/Item.kt new file mode 100644 index 0000000..082fa3f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/Item.kt @@ -0,0 +1,5 @@ +package net.helcel.cowspent.model + +interface Item { + fun isSection(): Boolean +} diff --git a/app/src/main/java/net/helcel/cowspent/model/ProjectType.kt b/app/src/main/java/net/helcel/cowspent/model/ProjectType.kt new file mode 100644 index 0000000..241471a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/ProjectType.kt @@ -0,0 +1,20 @@ +package net.helcel.cowspent.model + +enum class ProjectType(val id: String) { + LOCAL("l"), COSPEND("c"), IHATEMONEY("i"); + + companion object { + private val reverseMap: Map = HashMap() + + init { + for (type in entries) { + (reverseMap as MutableMap)[type.id] = type + } + } + + @JvmStatic + fun getTypeById(id: String?): ProjectType? { + return reverseMap[id] + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/model/SectionItem.kt b/app/src/main/java/net/helcel/cowspent/model/SectionItem.kt new file mode 100644 index 0000000..c70a0e9 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/SectionItem.kt @@ -0,0 +1,8 @@ +package net.helcel.cowspent.model + +class SectionItem(var title: String) : Item { + override fun isSection(): Boolean { + return true + } + +} diff --git a/app/src/main/java/net/helcel/cowspent/model/Transaction.kt b/app/src/main/java/net/helcel/cowspent/model/Transaction.kt new file mode 100644 index 0000000..5c5e646 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/Transaction.kt @@ -0,0 +1,3 @@ +package net.helcel.cowspent.model + +class Transaction(var owerMemberId: Long, var receiverMemberId: Long, var amount: Double) diff --git a/app/src/main/java/net/helcel/cowspent/model/UserItem.kt b/app/src/main/java/net/helcel/cowspent/model/UserItem.kt new file mode 100644 index 0000000..22b47a0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/UserItem.kt @@ -0,0 +1,3 @@ +package net.helcel.cowspent.model + +class UserItem(var id: Long, var name: String) \ No newline at end of file diff --git a/app/src/main/java/net/helcel/cowspent/model/parsed/AustrianBillQrCode.kt b/app/src/main/java/net/helcel/cowspent/model/parsed/AustrianBillQrCode.kt new file mode 100644 index 0000000..46d2da0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/parsed/AustrianBillQrCode.kt @@ -0,0 +1,5 @@ +package net.helcel.cowspent.model.parsed + +import java.util.Date + +class AustrianBillQrCode(val cashDeskId: String, val date: Date, val amount: Double) diff --git a/app/src/main/java/net/helcel/cowspent/model/parsed/CroatianBillQrCode.kt b/app/src/main/java/net/helcel/cowspent/model/parsed/CroatianBillQrCode.kt new file mode 100644 index 0000000..29d7e0f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/model/parsed/CroatianBillQrCode.kt @@ -0,0 +1,5 @@ +package net.helcel.cowspent.model.parsed + +import java.time.LocalDateTime + +data class CroatianBillQrCode(val date: LocalDateTime?, val amount: Double) diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt new file mode 100644 index 0000000..cc73377 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -0,0 +1,1248 @@ +package net.helcel.cowspent.persistence + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.text.TextUtils +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.preference.PreferenceManager +import net.helcel.cowspent.R +import net.helcel.cowspent.android.main.BillsListViewActivity +import net.helcel.cowspent.model.* +import net.helcel.cowspent.util.SupportUtil +import java.text.SimpleDateFormat +import java.util.* + +/** + * Helps to add, get, update and delete bills, members, projects with the option to trigger a sync with the server. + */ +class CowspentSQLiteOpenHelper private constructor(val context: Context) : + SQLiteOpenHelper(context, database_name, null, database_version) { + + val cowspentServerSyncHelper: CowspentServerSyncHelper = CowspentServerSyncHelper.getInstance(this) + + override fun onCreate(db: SQLiteDatabase) { + createTableMembers(db) + createTableBills(db) + createTableBillowers(db) + createTableProjects(db) + createTableAccountProjects(db) + createTableCategories(db) + createTablePaymentModes(db) + createTableCurrencies(db) + createIndexes(db) + } + + private fun createTableMembers(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_members ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId INTEGER, " + + "$key_projectid INTEGER, " + + "$key_name TEXT, " + + "$key_activated INTEGER, " + + "$key_weight FLOAT, " + + "$key_r INTEGER, " + + "$key_g INTEGER, " + + "$key_b INTEGER, " + + "$key_nc_userid TEXT, " + + "$key_avatar TEXT, " + + "$key_state INTEGER)" + ) + } + + private fun createTableProjects(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_projects ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId TEXT, " + + "$key_name TEXT, " + + "$key_ihmUrl TEXT, " + + "$key_password TEXT, " + + "$key_bearer_token TEXT, " + + "$key_currencyName TEXT, " + + "$key_deletionDisabled INTEGER, " + + "$key_myAccessLevel INTEGER DEFAULT ${DBProject.ACCESS_LEVEL_UNKNOWN}, " + + "$key_lastPayerId INTEGER, " + + "$key_lastSyncTimestamp INTEGER DEFAULT 0, " + + "$key_email TEXT, " + + "$key_type TEXT, " + + "$key_archived INTEGER DEFAULT 0)" + ) + } + + private fun createTableBills(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_bills ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId INTEGER, " + + "$key_projectid INTEGER, " + + "$key_payer_id INTEGER, " + + "$key_amount FLOAT, " + + "$key_what TEXT, " + + "$key_state INTEGER, " + + "$key_timestamp INTEGER, " + + "$key_payment_mode TEXT DEFAULT \"n\", " + + "$key_category_id INTEGER DEFAULT 0, " + + "$key_repeat TEXT, " + + "$key_comment TEXT DEFAULT \"\", " + + "$key_payment_mode_id INTEGER DEFAULT 0)" + ) + } + + private fun createTableBillowers(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_billowers ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_billId INTEGER, " + + "$key_member_id INTEGER)" + ) + } + + private fun createTableAccountProjects(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_account_projects ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId TEXT, " + + "$key_name TEXT, " + + "$key_ncUrl TEXT, " + + "$key_bearer_token TEXT, " + + "$key_password TEXT, " + + "$key_archived INTEGER DEFAULT 0)" + ) + } + + private fun createTableCategories(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_categories ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId INTEGER, " + + "$key_projectid INTEGER, " + + "$key_name TEXT, " + + "$key_icon TEXT, " + + "$key_color TEXT)" + ) + } + + private fun createTablePaymentModes(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_payment_modes ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId INTEGER, " + + "$key_projectid INTEGER, " + + "$key_name TEXT, " + + "$key_icon TEXT, " + + "$key_color TEXT)" + ) + } + + private fun createTableCurrencies(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE $table_currencies ( " + + "$key_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$key_remoteId INTEGER, " + + "$key_projectid INTEGER, " + + "$key_name TEXT, " + + "$key_exchangeRate FLOAT, " + + "$key_state INTEGER)" + ) + } + + @SuppressLint("Range") + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + if (oldVersion < 2) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_lastPayerId INTEGER DEFAULT 0") + } + if (oldVersion < 3) { + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_repeat TEXT") + } + if (oldVersion < 4) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_type TEXT") + val projects = getProjectsCustom("", arrayOf(), default_order, db) + for (project in projects) { + val url = project.serverUrl + project.type = if (url == null) ProjectType.LOCAL else ProjectType.COSPEND + updateProject( + project.id, project.name, project.email, + project.password, project.lastPayerId, project.type, + project.lastSyncedTimestamp, project.currencyName, + project.isDeletionDisabled, project.myAccessLevel, project.bearerToken, + project.archivedTs, db + ) + } + } + if (oldVersion < 5) { + createTableAccountProjects(db) + createIndex(db, table_account_projects) + } + if (oldVersion < 6) { + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_payment_mode TEXT DEFAULT \"n\"") + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_category_id INTEGER DEFAULT 0") + } + if (oldVersion < 7) { + db.execSQL("ALTER TABLE $table_members ADD COLUMN $key_r INTEGER DEFAULT NULL") + db.execSQL("ALTER TABLE $table_members ADD COLUMN $key_g INTEGER DEFAULT NULL") + db.execSQL("ALTER TABLE $table_members ADD COLUMN $key_b INTEGER DEFAULT NULL") + } + if (oldVersion < 8) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_lastSyncTimestamp INTEGER DEFAULT 0") + } + if (oldVersion < 9) { + createTableCategories(db) + createIndex(db, table_categories) + } + if (oldVersion < 10) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_currencyName TEXT") + createTableCurrencies(db) + createIndex(db, table_currencies) + } + if (oldVersion < 11) { + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_timestamp INTEGER") + val idToTs: MutableMap = HashMap() + val sdfDate = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) + val cursor = db.query(table_bills, arrayOf(key_id, "DATE"), "", arrayOf(), null, null, null) + val dateNow = Date() + while (cursor.moveToNext()) { + val id = cursor.getLong(cursor.getColumnIndex(key_id)) + val dateStr = cursor.getString(cursor.getColumnIndex("DATE")) + val date = try { + sdfDate.parse(dateStr) + } catch (_: Exception) { + dateNow + } + val timestamp = (date?.time ?: System.currentTimeMillis()) / 1000 + idToTs[id] = timestamp + } + cursor.close() + for (billId in idToTs.keys) { + val timestamp = idToTs[billId]!! + val values = ContentValues() + values.put(key_timestamp, timestamp) + db.update(table_bills, values, "$key_id = ?", arrayOf(billId.toString())) + } + } + if (oldVersion < 12) { + db.execSQL("ALTER TABLE $table_members ADD COLUMN $key_nc_userid TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE $table_members ADD COLUMN $key_avatar TEXT DEFAULT NULL") + } + if (oldVersion < 13) { + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_comment TEXT DEFAULT \"\"") + } + if (oldVersion < 14) { + val projects = getProjectsCustom("", arrayOf(), default_order, db) + for (project in projects) { + updateProject( + project.id, project.name, project.email, + project.password, project.lastPayerId, project.type, + 0L, project.currencyName, project.isDeletionDisabled, project.myAccessLevel, project.bearerToken, + project.archivedTs, db + ) + } + } + if (oldVersion < 15) { + db.execSQL("ALTER TABLE $table_bills ADD COLUMN $key_payment_mode_id INTEGER DEFAULT 0") + for (key in DBBill.oldPmIdToNew.keys) { + val values = ContentValues() + values.put(key_payment_mode_id, DBBill.oldPmIdToNew[key]) + db.update(table_bills, values, "$key_payment_mode = ?", arrayOf(key)) + } + createTablePaymentModes(db) + createIndex(db, table_payment_modes) + } + if (oldVersion < 16) { + db.execSQL("ALTER TABLE $table_currencies ADD COLUMN $key_state INTEGER DEFAULT ${DBBill.STATE_OK}") + } + if (oldVersion < 17) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_deletionDisabled INTEGER") + } + if (oldVersion < 18) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_myAccessLevel INTEGER DEFAULT ${DBProject.ACCESS_LEVEL_UNKNOWN}") + } + if (oldVersion < 19) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_bearer_token TEXT") + } + if (oldVersion < 20) { + db.execSQL("ALTER TABLE $table_projects ADD COLUMN $key_archived INTEGER DEFAULT 0") + db.execSQL("ALTER TABLE $table_account_projects ADD COLUMN $key_archived INTEGER DEFAULT 0") + } + } + + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + recreateDatabase(db) + } + + private fun recreateDatabase(db: SQLiteDatabase) { + dropIndexes(db) + db.execSQL("DROP TABLE $table_members") + db.execSQL("DROP TABLE $table_projects") + db.execSQL("DROP TABLE $table_bills") + db.execSQL("DROP TABLE $table_billowers") + db.execSQL("DROP TABLE $table_account_projects") + db.execSQL("DROP TABLE $table_categories") + db.execSQL("DROP TABLE $table_payment_modes") + db.execSQL("DROP TABLE $table_currencies") + onCreate(db) + } + + private fun dropIndexes(db: SQLiteDatabase) { + val c = db.query("sqlite_master", arrayOf("name"), "type=?", arrayOf("index"), null, null, null) + while (c.moveToNext()) { + db.execSQL("DROP INDEX " + c.getString(0)) + } + c.close() + } + + private fun createIndexes(db: SQLiteDatabase) { + createIndex(db, table_members) + createIndex(db, table_projects) + createIndex(db, table_bills) + createIndex(db, table_billowers) + createIndex(db, table_account_projects) + createIndex(db, table_categories) + createIndex(db, table_payment_modes) + createIndex(db, table_currencies) + } + + private fun createIndex(db: SQLiteDatabase, table: String) { + val indexName = "${table}_${key_id}_idx" + db.execSQL("CREATE INDEX IF NOT EXISTS $indexName ON $table($key_id)") + } + + fun addAccountProject(accountProject: DBAccountProject): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, accountProject.remoteId) + values.put(key_password, accountProject.password) + values.put(key_ncUrl, accountProject.ncUrl) + values.put(key_name, accountProject.name) + values.put(key_archived, accountProject.archivedTs ?: 0L) + return db.insert(table_account_projects, null, values) + } + + val accountProjects: List + get() = getAccountProjectsCustom("", arrayOf(), default_order) + + @WorkerThread + private fun getAccountProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + return getAccountProjectsCustom(selection, selectionArgs, orderBy, readableDatabase) + } + + @WorkerThread + private fun getAccountProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { + val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy) + val accountProjects: MutableList = ArrayList() + while (cursor.moveToNext()) { + accountProjects.add(getAccountProjectFromCursor(cursor)) + } + cursor.close() + return accountProjects + } + + @SuppressLint("Range") + private fun getAccountProjectFromCursor(cursor: Cursor): DBAccountProject { + val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived)) + return DBAccountProject( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getString(cursor.getColumnIndex(key_remoteId)), + cursor.getString(cursor.getColumnIndex(key_password)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getString(cursor.getColumnIndex(key_ncUrl)), + if (archivedTs > 0) archivedTs else null + ) + } + + fun clearAccountProjects() { + val db = writableDatabase + db.delete(table_account_projects, null, null) + } + + fun addPaymentMode(paymentMode: DBPaymentMode): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, paymentMode.remoteId) + values.put(key_projectid, paymentMode.projectId) + values.put(key_name, paymentMode.name) + values.put(key_icon, paymentMode.icon) + values.put(key_color, paymentMode.color) + return db.insert(table_payment_modes, null, values) + } + + fun getPaymentMode(remoteId: Long, projectId: Long): DBPaymentMode? { + val paymentModes = getPaymentModesCustom( + "$key_remoteId = ? AND $key_projectid = ?", + arrayOf(remoteId.toString(), projectId.toString()), + null + ) + return if (paymentModes.isEmpty()) null else paymentModes[0] + } + + fun getPaymentModes(projectId: Long): List { + return getPaymentModesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null) + } + + @WorkerThread + private fun getPaymentModesCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + return getPaymentModesCustom(selection, selectionArgs, orderBy, readableDatabase) + } + + @WorkerThread + private fun getPaymentModesCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { + val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy) + val paymentModes: MutableList = ArrayList() + while (cursor.moveToNext()) { + paymentModes.add(getPaymentModeFromCursor(cursor)) + } + cursor.close() + return paymentModes + } + + @SuppressLint("Range") + private fun getPaymentModeFromCursor(cursor: Cursor): DBPaymentMode { + return DBPaymentMode( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_remoteId)), + cursor.getLong(cursor.getColumnIndex(key_projectid)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getString(cursor.getColumnIndex(key_icon)), + cursor.getString(cursor.getColumnIndex(key_color)) + ) + } + + fun updatePaymentMode(id: Long, name: String?, icon: String?, color: String?) { + val db = writableDatabase + val values = ContentValues() + if (name != null) values.put(key_name, name) + if (icon != null) values.put(key_icon, icon) + if (color != null) values.put(key_color, color) + if (values.size() > 0) { + db.update(table_payment_modes, values, "$key_id = ?", arrayOf(id.toString())) + } + } + + fun deletePaymentMode(id: Long) { + val db = writableDatabase + db.delete(table_payment_modes, "$key_id = ?", arrayOf(id.toString())) + } + + fun addCategory(category: DBCategory): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, category.remoteId) + values.put(key_projectid, category.projectId) + values.put(key_name, category.name) + values.put(key_icon, category.icon) + values.put(key_color, category.color) + return db.insert(table_categories, null, values) + } + + fun getCategory(remoteId: Long, projectId: Long): DBCategory? { + val categories = getCategoriesCustom( + "$key_remoteId = ? AND $key_projectid = ?", + arrayOf(remoteId.toString(), projectId.toString()), + null + ) + return if (categories.isEmpty()) null else categories[0] + } + + fun getCategories(projectId: Long): List { + return getCategoriesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null) + } + + @WorkerThread + private fun getCategoriesCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + return getCategoriesCustom(selection, selectionArgs, orderBy, readableDatabase) + } + + @WorkerThread + private fun getCategoriesCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { + val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy) + val categories: MutableList = ArrayList() + while (cursor.moveToNext()) { + categories.add(getCategoryFromCursor(cursor)) + } + cursor.close() + return categories + } + + @SuppressLint("Range") + private fun getCategoryFromCursor(cursor: Cursor): DBCategory { + return DBCategory( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_remoteId)), + cursor.getLong(cursor.getColumnIndex(key_projectid)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getString(cursor.getColumnIndex(key_icon)), + cursor.getString(cursor.getColumnIndex(key_color)) + ) + } + + fun updateCategory(id: Long, name: String?, icon: String?, color: String?) { + val db = writableDatabase + val values = ContentValues() + if (name != null) values.put(key_name, name) + if (icon != null) values.put(key_icon, icon) + if (color != null) values.put(key_color, color) + if (values.size() > 0) { + db.update(table_categories, values, "$key_id = ?", arrayOf(id.toString())) + } + } + + fun deleteCategory(id: Long) { + val db = writableDatabase + db.delete(table_categories, "$key_id = ?", arrayOf(id.toString())) + } + + fun addCurrency(currency: DBCurrency): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, currency.remoteId) + values.put(key_projectid, currency.projectId) + values.put(key_name, currency.name) + values.put(key_exchangeRate, currency.exchangeRate) + values.put(key_state, currency.state) + return db.insert(table_currencies, null, values) + } + + fun addCurrencyAndSync(m: DBCurrency) { + addCurrency(m) + val proj = getProject(m.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun syncIfRemote(proj: DBProject) { + if (!proj.isLocal) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val offlineMode = preferences.getBoolean(context.getString(R.string.pref_key_offline_mode), false) + if (!offlineMode) { + cowspentServerSyncHelper.scheduleSync(true, proj.id) + } + } + } + + fun getCurrency(remoteId: Long, projectId: Long): DBCurrency? { + val currencies = getCurrenciesCustom( + "$key_remoteId = ? AND $key_projectid = ?", + arrayOf(remoteId.toString(), projectId.toString()), + null + ) + return if (currencies.isEmpty()) null else currencies[0] + } + + fun getCurrency(id: Long): DBCurrency? { + val currencies = getCurrenciesCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (currencies.isEmpty()) null else currencies[0] + } + + fun updateCurrency(id: Long, name: String?, exchangeRate: Double?) { + val db = writableDatabase + val values = ContentValues() + if (name != null) values.put(key_name, name) + if (exchangeRate != null) values.put(key_exchangeRate, exchangeRate) + if (values.size() > 0) { + db.update(table_currencies, values, "$key_id = ?", arrayOf(id.toString())) + } + } + + fun deleteCurrency(id: Long) { + val db = writableDatabase + db.delete(table_currencies, "$key_id = ?", arrayOf(id.toString())) + } + + fun getCurrencies(projectId: Long): List { + return getCurrenciesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null) + } + + @WorkerThread + private fun getCurrenciesCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + return getCurrenciesCustom(selection, selectionArgs, orderBy, readableDatabase) + } + + @WorkerThread + private fun getCurrenciesCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { + val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy) + val currencies: MutableList = ArrayList() + while (cursor.moveToNext()) { + currencies.add(getCurrencyFromCursor(cursor)) + } + cursor.close() + return currencies + } + + @SuppressLint("Range") + private fun getCurrencyFromCursor(cursor: Cursor): DBCurrency { + return DBCurrency( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_remoteId)), + cursor.getLong(cursor.getColumnIndex(key_projectid)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getDouble(cursor.getColumnIndex(key_exchangeRate)), + cursor.getInt(cursor.getColumnIndex(key_state)) + ) + } + + fun setCurrencyStateSync(currencyId: Long, state: Int) { + setCurrencyState(currencyId, state) + val currency = getCurrency(currencyId) + if (currency != null) { + val project = getProject(currency.projectId) + if (project != null) syncIfRemote(project) + } + } + + fun setCurrencyState(currencyId: Long, state: Int) { + val db = writableDatabase + val values = ContentValues() + values.put(key_state, state) + db.update(table_currencies, values, "$key_id = ?", arrayOf(currencyId.toString())) + } + + fun addProject(project: DBProject): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, project.remoteId) + values.put(key_password, project.password) + values.put(key_bearer_token, project.bearerToken) + values.put(key_email, project.email) + values.put(key_name, project.name) + values.put(key_ihmUrl, project.serverUrl) + values.put(key_type, project.type.id) + values.put(key_archived, project.archivedTs ?: 0L) + return db.insert(table_projects, null, values) + } + + fun getProject(id: Long): DBProject? { + val projects = getProjectsCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (projects.isEmpty()) null else projects[0] + } + + val projects: List + get() = getProjectsCustom("", arrayOf(), default_order) + + @WorkerThread + private fun getProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + return getProjectsCustom(selection, selectionArgs, orderBy, readableDatabase) + } + + @WorkerThread + private fun getProjectsCustom(selection: String, selectionArgs: Array, orderBy: String?, db: SQLiteDatabase): List { + val cursor = db.query(table_projects, columnsProjects, selection, selectionArgs, null, null, orderBy) + val projects: MutableList = ArrayList() + while (cursor.moveToNext()) { + projects.add(getProjectFromCursor(cursor)) + } + cursor.close() + return projects + } + + @SuppressLint("Range") + private fun getProjectFromCursor(cursor: Cursor): DBProject { + val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived)) + return DBProject( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getString(cursor.getColumnIndex(key_remoteId)), + cursor.getString(cursor.getColumnIndex(key_password)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getString(cursor.getColumnIndex(key_ihmUrl)), + cursor.getString(cursor.getColumnIndex(key_email)), + cursor.getLong(cursor.getColumnIndex(key_lastPayerId)), + ProjectType.getTypeById(cursor.getString(cursor.getColumnIndex(key_type)))!!, + cursor.getLong(cursor.getColumnIndex(key_lastSyncTimestamp)), + cursor.getString(cursor.getColumnIndex(key_currencyName)), + cursor.getInt(cursor.getColumnIndex(key_deletionDisabled)) != 0, + cursor.getInt(cursor.getColumnIndex(key_myAccessLevel)), + cursor.getString(cursor.getColumnIndex(key_bearer_token)), + if (archivedTs > 0) archivedTs else null, + cursor.getLong(cursor.getColumnIndex(key_latest_bill_ts)) + ) + } + + fun deleteProject(id: Long) { + val db = writableDatabase + for (b in getBillsOfProject(id)) { + deleteBill(b.id) + } + db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString())) + db.delete(table_projects, "$key_id = ?", arrayOf(id.toString())) + } + + fun updateProject( + projId: Long, newName: String?, newEmail: String?, + newPassword: String?, newLastPayerId: Long?, + newLastSyncedTimestamp: Long?, + newCurrencyName: String?, newDeletionDisabled: Boolean?, + newMyAccessLevel: Int?, newBearerToken: String?, + newArchivedTs: Long? = null + ) { + val db = writableDatabase + val values = ContentValues() + if (newName != null) values.put(key_name, newName) + if (newEmail != null) values.put(key_email, newEmail) + if (newPassword != null) values.put(key_password, newPassword) + if (newBearerToken != null) values.put(key_bearer_token, newBearerToken) + if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId) + if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp) + if (newCurrencyName != null) values.put(key_currencyName, newCurrencyName) + if (newDeletionDisabled != null) values.put(key_deletionDisabled, if (newDeletionDisabled) 1 else 0) + if (newMyAccessLevel != null) values.put(key_myAccessLevel, newMyAccessLevel) + if (newArchivedTs != null) values.put(key_archived, newArchivedTs) + if (values.size() > 0) { + db.update(table_projects, values, "$key_id = ?", arrayOf(projId.toString())) + } + } + + fun updateProject( + projId: Long, newName: String?, newEmail: String?, + newPassword: String?, newLastPayerId: Long?, + projectType: ProjectType, newLastSyncedTimestamp: Long?, + newCurrencyName: String?, newDeletionDisabled: Boolean?, + newMyAccessLevel: Int?, newBearerToken: String?, + newArchivedTs: Long? = null + ) { + val db = writableDatabase + updateProject( + projId, newName, newEmail, newPassword, newLastPayerId, projectType, + newLastSyncedTimestamp, newCurrencyName, newDeletionDisabled, newMyAccessLevel, + newBearerToken, newArchivedTs, db + ) + } + + private fun updateProject( + projId: Long, newName: String?, newEmail: String?, + newPassword: String?, newLastPayerId: Long?, + projectType: ProjectType, newLastSyncedTimestamp: Long?, + newCurrencyName: String?, newDeletionDisabled: Boolean?, + newMyAccessLevel: Int?, newBearerToken: String?, + newArchivedTs: Long?, db: SQLiteDatabase + ) { + val values = ContentValues() + if (newName != null) values.put(key_name, newName) + if (newEmail != null) values.put(key_email, newEmail) + if (newPassword != null) values.put(key_password, newPassword) + if (newBearerToken != null) values.put(key_bearer_token, newBearerToken) + if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId) + if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp) + if (newCurrencyName != null) values.put(key_currencyName, newCurrencyName) + if (newDeletionDisabled != null) values.put(key_deletionDisabled, if (newDeletionDisabled) 1 else 0) + if (newMyAccessLevel != null) values.put(key_myAccessLevel, newMyAccessLevel) + if (newArchivedTs != null) values.put(key_archived, newArchivedTs) + values.put(key_type, projectType.id) + if (values.size() > 0) { + db.update(table_projects, values, "$key_id = ?", arrayOf(projId.toString())) + } + } + + fun addMember(m: DBMember): Long { + if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add member]") } + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, m.remoteId) + values.put(key_projectid, m.projectId) + values.put(key_name, m.name) + values.put(key_activated, if (m.isActivated) "1" else "0") + values.put(key_weight, m.weight) + values.put(key_state, m.state) + values.put(key_r, m.r) + values.put(key_g, m.g) + values.put(key_b, m.b) + values.put(key_nc_userid, m.ncUserId) + values.put(key_avatar, m.avatar) + return db.insert(table_members, null, values) + } + + fun addMemberAndSync(m: DBMember) { + addMember(m) + val proj = getProject(m.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun updateMember( + memberId: Long, newName: String?, newWeight: Double?, + newActivated: Boolean?, newState: Int?, newRemoteId: Long?, + newR: Int?, newG: Int?, newB: Int?, + newNcUserId: String?, newAvatar: String? + ) { + val db = writableDatabase + val values = ContentValues() + if (newName != null) values.put(key_name, newName) + if (newWeight != null) values.put(key_weight, newWeight) + if (newRemoteId != null) values.put(key_remoteId, newRemoteId) + if (newActivated != null) values.put(key_activated, if (newActivated) 1 else 0) + if (newState != null) values.put(key_state, newState) + if (newR != null) values.put(key_r, newR) + if (newG != null) values.put(key_g, newG) + if (newB != null) values.put(key_b, newB) + if (newNcUserId != null) values.put(key_nc_userid, newNcUserId) + if (newAvatar != null) values.put(key_avatar, newAvatar) + if (values.size() > 0) { + db.update(table_members, values, "$key_id = ?", arrayOf(memberId.toString())) + } + } + + fun updateMemberAndSync( + m: DBMember, newName: String?, newWeight: Double?, + newActivated: Boolean?, + newR: Int?, newG: Int?, newB: Int?, + newNcUserId: String?, newAvatar: String? + ) { + val newState = if (m.state == DBBill.STATE_ADDED) DBBill.STATE_ADDED else DBBill.STATE_EDITED + updateMember( + m.id, newName, newWeight, newActivated, newState, null, + newR, newG, newB, newNcUserId, newAvatar + ) + Log.v(TAG, "UPDATE MEMBER AND SYNC") + val proj = getProject(m.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun getMembersOfProject(projId: Long, orderByParam: String?): List { + val orderBy = orderByParam ?: "LOWER($key_name)" + return getMembersCustom("$key_projectid = ?", arrayOf(projId.toString()), "$orderBy ASC") + } + + fun getMembersOfProjectWithState(projId: Long, state: Int): List { + return getMembersCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.toString()), + "$key_name ASC" + ) + } + + fun getActivatedMembersOfProject(projId: Long): List { + return getMembersCustom( + "$key_projectid = ? AND $key_activated = 1", + arrayOf(projId.toString()), + "$key_name ASC" + ) + } + + fun getMember(remoteId: Long, projId: Long): DBMember? { + val members = getMembersCustom( + "$key_remoteId = ? AND $key_projectid = ?", + arrayOf(remoteId.toString(), projId.toString()), + null + ) + return if (members.isEmpty()) null else members[0] + } + + fun getMember(id: Long): DBMember? { + val members = getMembersCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (members.isEmpty()) null else members[0] + } + + @WorkerThread + private fun getMembersCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + val db = readableDatabase + val cursor = db.query(table_members, columnsMembers, selection, selectionArgs, null, null, orderBy) + val members: MutableList = ArrayList() + while (cursor.moveToNext()) { + members.add(getMemberFromCursor(cursor)) + } + cursor.close() + return members + } + + @SuppressLint("Range") + private fun getMemberFromCursor(cursor: Cursor): DBMember { + return DBMember( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_remoteId)), + cursor.getLong(cursor.getColumnIndex(key_projectid)), + cursor.getString(cursor.getColumnIndex(key_name)), + cursor.getInt(cursor.getColumnIndex(key_activated)) == 1, + cursor.getDouble(cursor.getColumnIndex(key_weight)), + cursor.getInt(cursor.getColumnIndex(key_state)), + if (cursor.isNull(cursor.getColumnIndex(key_r))) null else cursor.getInt(cursor.getColumnIndex(key_r)), + if (cursor.isNull(cursor.getColumnIndex(key_g))) null else cursor.getInt(cursor.getColumnIndex(key_g)), + if (cursor.isNull(cursor.getColumnIndex(key_b))) null else cursor.getInt(cursor.getColumnIndex(key_b)), + if (cursor.isNull(cursor.getColumnIndex(key_nc_userid))) null else cursor.getString(cursor.getColumnIndex(key_nc_userid)), + if (cursor.isNull(cursor.getColumnIndex(key_avatar))) null else cursor.getString(cursor.getColumnIndex(key_avatar)) + ) + } + + fun deleteMember(id: Long) { + val db = writableDatabase + db.delete(table_members, "$key_id = ?", arrayOf(id.toString())) + } + + fun addBill(b: DBBill): Long { + if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add bill]") } + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, b.remoteId) + values.put(key_projectid, b.projectId) + values.put(key_payer_id, b.payerId) + values.put(key_amount, b.amount) + values.put(key_timestamp, b.timestamp) + values.put(key_what, b.what) + values.put(key_state, b.state) + values.put(key_repeat, b.repeat) + values.put(key_payment_mode, b.paymentMode) + values.put(key_payment_mode_id, b.paymentModeRemoteId) + values.put(key_category_id, b.categoryRemoteId) + values.put(key_comment, b.comment) + val billId = db.insert(table_bills, null, values) + for (bo in b.billOwers) { + addBillower(billId, bo.memberId) + } + return billId + } + + fun setBillState(billId: Long, state: Int) { + val db = writableDatabase + val values = ContentValues() + values.put(key_state, state) + db.update(table_bills, values, "$key_id = ?", arrayOf(billId.toString())) + } + + fun updateBill( + billId: Long, newRemoteId: Long?, newPayerId: Long?, + newAmount: Double?, newTimestamp: Long?, + newWhat: String?, newState: Int?, + newRepeat: String?, + newPaymentMode: String?, newPaymentModeRemoteId: Int?, + newCategoryId: Int?, newComment: String? + ) { + val db = writableDatabase + val values = ContentValues() + if (newTimestamp != null) values.put(key_timestamp, newTimestamp) + if (newWhat != null) values.put(key_what, newWhat) + if (newRemoteId != null) values.put(key_remoteId, newRemoteId) + if (newPayerId != null) values.put(key_payer_id, newPayerId) + if (newAmount != null) values.put(key_amount, newAmount) + if (newState != null) values.put(key_state, newState) + if (newRepeat != null) values.put(key_repeat, newRepeat) + if (newPaymentMode != null) values.put(key_payment_mode, newPaymentMode) + if (newPaymentModeRemoteId != null) values.put(key_payment_mode_id, newPaymentModeRemoteId) + if (newCategoryId != null) values.put(key_category_id, newCategoryId) + if (newComment != null) values.put(key_comment, newComment) + if (values.size() > 0) { + db.update(table_bills, values, "$key_id = ?", arrayOf(billId.toString())) + } + } + + fun updateBillAndSync( + bill: DBBill, newPayerId: Long, newAmount: Double, + newTimestamp: Long?, newWhat: String?, + newOwersIds: List?, newRepeat: String?, + newPaymentMode: String?, newPaymentModeRemoteId: Int?, + newCategoryId: Int?, + newComment: String? + ) { + val newState = if (bill.state == DBBill.STATE_ADDED) DBBill.STATE_ADDED else DBBill.STATE_EDITED + updateBill( + bill.id, null, newPayerId, newAmount, newTimestamp, newWhat, newState, + newRepeat, newPaymentMode, newPaymentModeRemoteId, newCategoryId, newComment + ) + val dbBillOwers = getBillowersOfBill(bill.id) + val dbBillOwersByMemberId: MutableMap = HashMap() + for (bo in dbBillOwers) { + dbBillOwersByMemberId[bo.memberId] = bo + } + if (newOwersIds != null) { + for (newOwerId in newOwersIds) { + if (!dbBillOwersByMemberId.containsKey(newOwerId)) { + addBillower(bill.id, newOwerId) + } + } + for (dbBo in dbBillOwers) { + if (!newOwersIds.contains(dbBo.memberId)) { + deleteBillOwer(dbBo.id) + } + } + } + Log.v(TAG, "UPDATE BILL AND SYNC") + val proj = getProject(bill.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun getBillsOfProject(projId: Long): List { + return getBillsCustom("$key_projectid = ?", arrayOf(projId.toString()), "$key_timestamp ASC") + } + + fun getBillsOfProjectWithState(projId: Long, state: Int): List { + return getBillsCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.toString()), + "$key_timestamp ASC" + ) + } + + fun getBillsOfMember(memberId: Long): List { + return getBillsCustom("$key_payer_id = ?", arrayOf(memberId.toString()), "$key_timestamp ASC") + } + + @Suppress("unused") + fun getBill(remoteId: Long, projId: Long): DBBill? { + val bills = getBillsCustom( + "$key_remoteId = ? AND $key_projectid = ?", + arrayOf(remoteId.toString(), projId.toString()), + null + ) + return if (bills.isEmpty()) null else bills[0] + } + + fun getBill(billId: Long): DBBill? { + val bills = getBillsCustom("$key_id = ?", arrayOf(billId.toString()), null) + return if (bills.isEmpty()) null else bills[0] + } + + fun getCurrenciesOfProjectWithState(projId: Long, state: Int): List { + return getCurrenciesCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.toString()), + null + ) + } + + @WorkerThread + fun searchBills(query: CharSequence?, projectId: Long): List { + val andWhere: MutableList = ArrayList() + val args: MutableList = ArrayList() + andWhere.add("($key_projectid = $projectId)") + andWhere.add("($key_state != ${DBBill.STATE_DELETED})") + if (query != null) { + args.add("%$query%") + var whereStr = "($key_what LIKE ?" + if (SupportUtil.isDouble(query.toString())) { + whereStr += " OR ($key_amount <= (? + 10) AND $key_amount >= (? - 10))" + args.add(query.toString()) + args.add(query.toString()) + } + val members = getMembersOfProject(projectId, null) + val memberNames: MutableList = ArrayList() + val memberIds: MutableList = ArrayList() + for (m in members) { + memberNames.add(m.name.lowercase(Locale.ROOT)) + memberIds.add(m.id) + } + val queryStr = query.toString() + val words = queryStr.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var nameSql = "" + for (word in words) { + if (word.startsWith("+")) { + val nameQuery = word.replace("^\\+".toRegex(), "") + val memberIndex = memberNames.indexOf(nameQuery.lowercase(Locale.ROOT)) + if (memberIndex != -1) { + val searchMemberId = memberIds[memberIndex] + nameSql += "($key_payer_id=?) AND " + args.add(searchMemberId.toString()) + } + } + if (word.startsWith("-")) { + val nameQuery = word.replace("^-".toRegex(), "") + val memberIndex = memberNames.indexOf(nameQuery.lowercase(Locale.ROOT)) + if (memberIndex != -1) { + val searchMemberId = memberIds[memberIndex] + val joinOwer = "select $table_bills.$key_id from $table_bills inner join $table_billowers " + + "where $key_member_id=? and $table_bills.$key_id=$table_billowers.$key_billId" + nameSql += "($key_id IN ($joinOwer)) AND " + args.add(searchMemberId.toString()) + } + } + if (word.startsWith("@")) { + val nameQuery = word.replace("^@".toRegex(), "") + val memberIndex = memberNames.indexOf(nameQuery.lowercase(Locale.ROOT)) + if (memberIndex != -1) { + val searchMemberId = memberIds[memberIndex] + nameSql += "( ($key_payer_id=?) OR " + args.add(searchMemberId.toString()) + val joinOwer = "select $table_bills.$key_id from $table_bills inner join $table_billowers " + + "where $key_member_id=? and $table_bills.$key_id=$table_billowers.$key_billId" + nameSql += "($key_id IN ($joinOwer)) ) AND " + args.add(searchMemberId.toString()) + } + } + } + if (nameSql != "") { + nameSql = nameSql.replace(" AND $".toRegex(), "") + whereStr += " OR ($nameSql)" + } + whereStr += ")" + andWhere.add(whereStr) + } + val order = "$key_timestamp DESC" + return getBillsCustom(TextUtils.join(" AND ", andWhere), args.toTypedArray(), order) + } + + @WorkerThread + private fun getBillsCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + val db = readableDatabase + val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy) + val bills: MutableList = ArrayList() + while (cursor.moveToNext()) { + val bill = getBillFromCursor(cursor) + bill.billOwers = getBillowersOfBill(bill.id) + bills.add(bill) + } + cursor.close() + return bills + } + + @SuppressLint("Range") + private fun getBillFromCursor(cursor: Cursor): DBBill { + return DBBill( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_remoteId)), + cursor.getLong(cursor.getColumnIndex(key_projectid)), + cursor.getLong(cursor.getColumnIndex(key_payer_id)), + cursor.getDouble(cursor.getColumnIndex(key_amount)), + cursor.getLong(cursor.getColumnIndex(key_timestamp)), + cursor.getString(cursor.getColumnIndex(key_what)), + cursor.getInt(cursor.getColumnIndex(key_state)), + cursor.getString(cursor.getColumnIndex(key_repeat)), + cursor.getString(cursor.getColumnIndex(key_payment_mode)), + cursor.getInt(cursor.getColumnIndex(key_category_id)), + cursor.getString(cursor.getColumnIndex(key_comment)), + cursor.getInt(cursor.getColumnIndex(key_payment_mode_id)) + ) + } + + fun deleteBill(id: Long) { + val db = writableDatabase + db.delete(table_billowers, "$key_billId = ?", arrayOf(id.toString())) + db.delete(table_bills, "$key_id = ?", arrayOf(id.toString())) + } + + fun addBillower(billId: Long, memberId: Long) { + if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add billower]") } + val db = writableDatabase + val values = ContentValues() + values.put(key_billId, billId) + values.put(key_member_id, memberId) + db.insert(table_billowers, null, values) + } + + fun getBillowersOfBill(billId: Long): List { + return getBillOwersCustom("$key_billId = ?", arrayOf(billId.toString()), null) + } + + fun getBillowersOfMember(memberId: Long): List { + return getBillOwersCustom("$key_member_id = ?", arrayOf(memberId.toString()), null) + } + + @WorkerThread + private fun getBillOwersCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + val db = readableDatabase + val cursor = db.query(table_billowers, columnsBillowers, selection, selectionArgs, null, null, orderBy) + val billOwers: MutableList = ArrayList() + while (cursor.moveToNext()) { + billOwers.add(getBillOwerFromCursor(cursor)) + } + cursor.close() + return billOwers + } + + @SuppressLint("Range") + private fun getBillOwerFromCursor(cursor: Cursor): DBBillOwer { + return DBBillOwer( + cursor.getLong(cursor.getColumnIndex(key_id)), + cursor.getLong(cursor.getColumnIndex(key_billId)), + cursor.getLong(cursor.getColumnIndex(key_member_id)) + ) + } + + fun deleteBillOwer(id: Long) { + val db = writableDatabase + db.delete(table_billowers, "$key_id = ?", arrayOf(id.toString())) + } + + @Suppress("ConstPropertyName") + companion object { + private val TAG = CowspentSQLiteOpenHelper::class.java.simpleName + private const val database_version = 20 + private const val database_name = "COWSPENT" + private const val table_members = "MEMBERS" + const val key_id = "ID" + const val key_remoteId = "REMOTEID" + private const val key_projectid = "PROJECTID" + const val key_name = "NAME" + private const val key_activated = "ACTIVATED" + private const val key_weight = "WEIGHT" + private const val key_state = "STATE" + private const val key_r = "R" + private const val key_g = "G" + private const val key_b = "B" + const val key_nc_userid = "NCUSERID" + const val key_avatar = "AVATAR" + private const val table_projects = "PROJECTS" + private const val key_email = "EMAIL" + private const val key_password = "PASSWORD" + private const val key_bearer_token = "BEARERTOKEN" + private const val key_ihmUrl = "IHMURL" + private const val key_lastPayerId = "LASTPAYERID" + private const val key_type = "TYPE" + private const val key_lastSyncTimestamp = "LASTSYNCED" + private const val key_currencyName = "CURRENCYNAME" + private const val key_deletionDisabled = "DELETIONDISABLED" + private const val key_myAccessLevel = "MYACCESSLEVEL" + private const val key_archived = "ARCHIVED" + private const val table_bills = "BILLS" + private const val key_payer_id = "PAYERID" + private const val key_amount = "AMOUNT" + private const val key_timestamp = "TIMESTAMP" + private const val key_what = "WHAT" + private const val key_repeat = "REPEAT" + private const val key_payment_mode = "PAYMENTMODE" + private const val key_payment_mode_id = "PAYMENTMODEID" + private const val key_category_id = "CATEGORYID" + private const val key_comment = "COMMENT" + private const val table_billowers = "BILLOWERS" + private const val key_billId = "BILLID" + private const val key_member_id = "MEMBERID" + private const val table_account_projects = "ACCOUNTPROJECTS" + private const val key_ncUrl = "NCURL" + private const val table_categories = "CATEGORIES" + private const val key_icon = "ICON" + private const val key_color = "COLOR" + private const val table_payment_modes = "PAYMENTMODES" + private const val key_latest_bill_ts = "LATEST_BILL_TS" + private const val table_currencies = "CURRENCIES" + private const val key_exchangeRate = "EXCHANGERATE" + + private val columnsMembers = arrayOf( + key_id, key_remoteId, key_projectid, key_name, key_activated, key_weight, key_state, + key_r, key_g, key_b, key_nc_userid, key_avatar + ) + private val columnsProjects = arrayOf( + key_id, key_remoteId, key_password, key_name, key_ihmUrl, + key_email, key_lastPayerId, key_type, key_lastSyncTimestamp, key_currencyName, + key_deletionDisabled, key_myAccessLevel, key_bearer_token, key_archived, + "(SELECT MAX($key_timestamp) FROM $table_bills WHERE $key_projectid = $table_projects.$key_id AND $key_state != ${DBBill.STATE_DELETED}) AS $key_latest_bill_ts" + ) + private val columnsBills = arrayOf( + key_id, key_remoteId, key_projectid, key_payer_id, key_amount, + key_timestamp, key_what, key_state, key_repeat, key_payment_mode, key_category_id, + key_comment, key_payment_mode_id + ) + private val columnsBillowers = arrayOf( + key_id, key_billId, key_member_id + ) + private val columnsAccountProjects = arrayOf( + key_id, key_remoteId, key_password, key_name, key_ncUrl, key_archived + ) + private val columnsCategories = arrayOf( + key_id, key_remoteId, key_projectid, key_name, key_icon, key_color + ) + private val columnsPaymentModes = arrayOf( + key_id, key_remoteId, key_projectid, key_name, key_icon, key_color + ) + private val columnsCurrencies = arrayOf( + key_id, key_remoteId, key_projectid, key_name, key_exchangeRate, key_state + ) + private const val default_order = "$key_id DESC" + + @Volatile + private var instance: CowspentSQLiteOpenHelper? = null + + @JvmStatic + fun getInstance(context: Context): CowspentSQLiteOpenHelper { + return instance ?: synchronized(this) { + instance ?: CowspentSQLiteOpenHelper(context.applicationContext).also { instance = it } + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt new file mode 100644 index 0000000..fab690b --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -0,0 +1,1501 @@ +package net.helcel.cowspent.persistence + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.os.IBinder +import android.util.Log +import androidx.core.content.edit +import androidx.core.graphics.toColorInt +import androidx.preference.PreferenceManager +import com.google.gson.GsonBuilder +import com.nextcloud.android.sso.api.NextcloudAPI +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException +import com.nextcloud.android.sso.exceptions.TokenMismatchException +import com.nextcloud.android.sso.helper.SingleAccountHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.helcel.cowspent.R +import net.helcel.cowspent.android.account.AccountActivity +import net.helcel.cowspent.android.main.BillsListViewActivity +import net.helcel.cowspent.android.main.MainConstants +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.util.CospendClientUtil.LoginStatus +import net.helcel.cowspent.util.ICallback +import net.helcel.cowspent.util.IProjectCreationCallback +import net.helcel.cowspent.util.NextcloudClient +import net.helcel.cowspent.util.ServerResponse +import net.helcel.cowspent.util.SupportUtil +import net.helcel.cowspent.util.VersatileProjectSyncClient +import org.json.JSONException +import java.io.IOException + +@Suppress("DEPRECATION") +class CowspentServerSyncHelper private constructor(private val dbHelper: CowspentSQLiteOpenHelper) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val appContext: Context = dbHelper.context.applicationContext + private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(dbHelper.context) + private var networkConnected = false + + private val certService = object : ServiceConnection { + override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) { + if (isSyncPossible) { + val lastId = PreferenceManager.getDefaultSharedPreferences(dbHelper.context).getLong("selected_project", 0) + if (lastId != 0L) { + val proj = dbHelper.getProject(lastId) + if (proj != null) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_SYNC_PROJECT)) + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_NETWORK_AVAILABLE)) + } + } + } + } + + override fun onServiceDisconnected(p0: ComponentName?) { + } + } + + private var syncActive = false + private var syncAccountProjectsActive = false + + private var callbacksPush: MutableList = ArrayList() + private var callbacksPull: MutableList = ArrayList() + + init { + + updateNetworkStatus() + } + + @Throws(Throwable::class) + protected fun finalize() { + appContext.unbindService(certService) + } + + val isSyncPossible: Boolean + get() { + updateNetworkStatus() + return networkConnected + } + + fun addCallbackPull(callback: ICallback) { + callbacksPull.add(callback) + } + + fun scheduleSync(onlyLocalChanges: Boolean, projId: Long): SyncTask? { + Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}) ...") + updateNetworkStatus() + if (isSyncPossible && (!syncActive || onlyLocalChanges)) { + val project = dbHelper.getProject(projId) + if (project != null) { + Log.d(TAG, "... starting now") + val syncTask = SyncTask(onlyLocalChanges, project) + syncTask.addCallbacks(callbacksPush) + callbacksPush = ArrayList() + if (!onlyLocalChanges) { + syncTask.addCallbacks(callbacksPull) + callbacksPull = ArrayList() + } + return syncTask.execute() + } else { + Log.d(TAG, "sync asked for project $projId which does not exist : DOING NOTHING") + } + } else if (!onlyLocalChanges) { + Log.d(TAG, "... scheduled") + projectIdsToSync.add(projId) + for (callback in callbacksPush) { + callback.onScheduled() + } + } else { + Log.d(TAG, "... do nothing") + for (callback in callbacksPush) { + callback.onScheduled() + } + } + return null + } + + private fun updateNetworkStatus() { + val connMgr = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeInfo = connMgr.activeNetworkInfo + if (activeInfo != null && activeInfo.isConnected) { + Log.d(TAG, "Network connection established.") + networkConnected = true + } else { + networkConnected = false + Log.d(TAG, "No network connection.") + } + } + + inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject) { + private val callbacks: MutableList = ArrayList() + private var nextcloudClient: NextcloudClient? = null + private var client: VersatileProjectSyncClient? = null + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + private var nbPulledNewBills = 0 + private var nbPulledUpdatedBills = 0 + private var nbPulledDeletedBills = 0 + private var newBillsDialogText = "" + private var updatedBillsDialogText = "" + private var deletedBillsDialogText = "" + + private var deferred: Deferred? = null + + init { + Log.i(TAG, "SYNC TASK project : ${project.remoteId}") + } + + fun addCallbacks(callbacks: List) { + this.callbacks.addAll(callbacks) + } + + fun execute(): SyncTask { + deferred = scope.async { + syncActive = true + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + syncActive = false + status + } + return this + } + + private fun doWork(): LoginStatus { + var version: String? = null + if (project.type == ProjectType.COSPEND) { + nextcloudClient = createNextcloudClient() + if (nextcloudClient != null) { + try { + val response = nextcloudClient!!.getCapabilities(project) + version = response.cospendVersion + } catch (e: Exception) { + Log.i(TAG, "Failed to get cospend version when syncing: $e") + } + } else if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + return LoginStatus.SSO_TOKEN_MISMATCH + } + } + + Log.i(TAG, "Syncing, cospend version is: $version") + + client = createVersatileProjectSyncClient(version) + if (client == null) { + return LoginStatus.CONNECTION_FAILED + } + Log.i(TAG, "STARTING SYNCHRONIZATION with Cospend version($version)") + var status = pushLocalChanges() + if (status == LoginStatus.OK) { + status = pullRemoteChanges() + } + Log.i(TAG, "SYNCHRONIZATION FINISHED") + return status + } + + fun get(): LoginStatus = runBlocking { + deferred?.await() ?: LoginStatus.CONNECTION_FAILED + } + + private fun pushLocalChanges(): LoginStatus { + Log.d(TAG, "PUSH LOCAL CHANGES") + + return try { + val membersResponse = client!!.getMembers(project) + val remoteMembers = membersResponse.getMembers(project.id) + val remoteMembersNames = remoteMembers.map { it.name } + + val membersToAdd = dbHelper.getMembersOfProjectWithState(project.id, DBBill.STATE_ADDED) + for (mToAdd in membersToAdd) { + val searchIndex = remoteMembersNames.indexOf(mToAdd.name) + if (searchIndex != -1) { + val remoteMember = remoteMembers[searchIndex] + dbHelper.updateMember( + mToAdd.id, null, + remoteMember.weight, remoteMember.isActivated, + DBBill.STATE_OK, remoteMember.remoteId, remoteMember.r, + remoteMember.g, remoteMember.b, + remoteMember.ncUserId, "" + ) + } else { + val createRemoteMemberResponse = client!!.createRemoteMember(project, mToAdd) + val newRemoteId = createRemoteMemberResponse.remoteMemberId + if (newRemoteId > 0) { + dbHelper.updateMember( + mToAdd.id, null, + null, null, DBBill.STATE_OK, newRemoteId, + null, null, null, null, null + ) + } + } + } + + val membersToEdit = dbHelper.getMembersOfProjectWithState(project.id, DBBill.STATE_EDITED) + for (mToEdit in membersToEdit) { + try { + val editRemoteMemberResponse = client!!.editRemoteMember(project, mToEdit) + val remoteId = editRemoteMemberResponse.getRemoteId(project.id) + if (remoteId == mToEdit.remoteId) { + dbHelper.updateMember( + mToEdit.id, null, + null, null, DBBill.STATE_OK, null, + null, null, null, null, null + ) + } + } catch (e: IOException) { + if (e.message == "{\"message\": \"Internal Server Error\"}") { + Log.d(TAG, "EDIT MEMBER FAILED : it does not exist remotely") + } else { + throw e + } + } + } + + val members = dbHelper.getMembersOfProject(project.id, null) + val memberIdToRemoteId = members.associate { it.id to it.remoteId } + + val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED) + for (bToDel in toDelete) { + try { + val deleteRemoteBillResponse = client!!.deleteRemoteBill(project, bToDel.remoteId) + if (deleteRemoteBillResponse.stringContent == "OK") { + Log.d(TAG, "successfully deleted bill on remote project : delete it locally") + dbHelper.deleteBill(bToDel.id) + } + } catch (e: IOException) { + if (e.message == "\"Not Found\"") { + Log.d(TAG, "failed to delete bill on remote project : delete it locally anyway") + dbHelper.deleteBill(bToDel.id) + } else { + throw e + } + } catch (e: NextcloudHttpRequestFailedException) { + if (e.statusCode == 404) { + Log.d(TAG, "failed to delete bill on remote project : delete it locally anyway") + dbHelper.deleteBill(bToDel.id) + } else { + throw e + } + } + } + + val toEdit = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_EDITED) + for (bToEdit in toEdit) { + try { + val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId) + if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) { + dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK) + Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})") + } else { + Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})") + } + } catch (_: Exception) { + Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely") + } + } + + val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED) + for (bToAdd in toAdd) { + val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId) + val newRemoteId = createRemoteBillResponse.stringContent.toLong() + if (newRemoteId > 0) { + dbHelper.updateBill( + bToAdd.id, newRemoteId, null, + null, null, null, + DBBill.STATE_OK, null, + null, null, + null, null + ) + } + } + + val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED) + for (cToDel in currenciesToDelete) { + try { + val deleteRemoteCurrencyResponse = client!!.deleteRemoteCurrency(project, cToDel.remoteId) + if (deleteRemoteCurrencyResponse.stringContent == "OK") { + Log.d(TAG, "successfully deleted currency on remote project : delete it locally") + dbHelper.deleteCurrency(cToDel.id) + } + } catch (e: IOException) { + if (e.message == "\"Not Found\"") { + Log.d(TAG, "failed to delete currency on remote project : delete it locally anyway") + dbHelper.deleteCurrency(cToDel.id) + } else { + throw e + } + } + } + + val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED) + for (cToEdit in currenciesToEdit) { + try { + val editRemoteCurrencyResponse = client!!.editRemoteCurrency(project, cToEdit) + if (editRemoteCurrencyResponse.stringContent == cToEdit.remoteId.toString()) { + dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK) + Log.d(TAG, "SUCCESSFUL remote currency edition (${editRemoteCurrencyResponse.stringContent})") + } else { + Log.d(TAG, "FAILED to edit remote currency (${editRemoteCurrencyResponse.stringContent})") + } + } catch (e: IOException) { + if (e.message == "{\"message\": \"Internal Server Error\"}") { + Log.d(TAG, "FAILED to edit remote currency : it does not exist remotely") + } else { + throw e + } + } + } + + val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED) + for (cToAdd in currencyToAdd) { + val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd) + val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong() + if (newRemoteId > 0) { + dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK) + } + } + + LoginStatus.OK + } catch (_: ServerResponse.NotModifiedException) { + Log.d(TAG, "No changes, nothing to do.") + LoginStatus.OK + } catch (e: IOException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + Log.e(TAG, "Catch MISMATCHTOKEN", e) + LoginStatus.SSO_TOKEN_MISMATCH + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "Catch SSO HTTP req FAILED", e) + errorMessages.add(getErrorMessageFromException(e)) + e.cause?.let { exceptions.add(it) } + LoginStatus.REQ_FAILED + } + } + + private fun pullRemoteChanges(): LoginStatus { + Log.d(TAG, "pullRemoteChanges($project)") + val lastETag: String? = null + val lastModified: Long = 0 + return try { + val projResponse = client!!.getProject(project, lastModified, lastETag) + Log.d(TAG,projResponse.toString()) + val name = projResponse.name + Log.i(TAG, "AAA getProjectInfo, project name: $name") + val email = projResponse.email + val currencyName = projResponse.currencyName + val deletionDisabled = projResponse.deletionDisabled + val myAccessLevel = projResponse.myAccessLevel + val archivedTs = projResponse.archivedTs + + if (project.name == "" || name != project.name || project.email == null || project.email == "" || project.isDeletionDisabled != deletionDisabled || project.myAccessLevel != myAccessLevel || project.archivedTs != archivedTs || (project.currencyName == null) || (currencyName != project.currencyName) || email != project.email + ) { + Log.d(TAG, "update local project : $project") + project.name = name + project.currencyName = currencyName + project.isDeletionDisabled = deletionDisabled + project.myAccessLevel = myAccessLevel + project.archivedTs = archivedTs + dbHelper.updateProject( + projId = project.id, + newName = name, + newEmail = email, + newPassword = null, + newLastPayerId = null, + newLastSyncedTimestamp = null, + newCurrencyName = currencyName, + newDeletionDisabled = deletionDisabled, + newMyAccessLevel = myAccessLevel, + newBearerToken = null, + newArchivedTs = archivedTs ?: 0L + ) + } + + val remotePaymentModes = projResponse.getPaymentModes(project.id) + val remotePaymentModesByRemoteId = remotePaymentModes.associateBy { it.remoteId } + + for (pm in remotePaymentModes) { + val localPaymentMode = dbHelper.getPaymentMode(pm.remoteId, project.id) + if (localPaymentMode == null) { + Log.d(TAG, "Add local pm : $pm") + dbHelper.addPaymentMode(pm) + } else { + if (pm.name == localPaymentMode.name && + pm.color == localPaymentMode.color && + pm.icon == localPaymentMode.icon + ) { + Log.d(TAG, "Nothing to do for pm : $localPaymentMode") + } else { + Log.d(TAG, "Update local pm : $pm") + dbHelper.updatePaymentMode(localPaymentMode.id, pm.name, pm.icon, pm.color) + } + } + } + + val localPaymentModes = dbHelper.getPaymentModes(project.id) + for (localPaymentMode in localPaymentModes) { + if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) { + dbHelper.deletePaymentMode(localPaymentMode.id) + Log.d(TAG, "Delete local pm : $localPaymentMode") + } + } + + val remoteCategories = projResponse.getCategories(project.id) + val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId } + + for (c in remoteCategories) { + val localCategory = dbHelper.getCategory(c.remoteId, project.id) + if (localCategory == null) { + Log.d(TAG, "Add local category : $c") + dbHelper.addCategory(c) + } else { + if (c.name == localCategory.name && + c.color == localCategory.color && + c.icon == localCategory.icon + ) { + Log.d(TAG, "Nothing to do for category : $localCategory") + } else { + Log.d(TAG, "Update local category : $c") + dbHelper.updateCategory(localCategory.id, c.name, c.icon, c.color) + } + } + } + + val localCategories = dbHelper.getCategories(project.id) + for (localCategory in localCategories) { + if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) { + dbHelper.deleteCategory(localCategory.id) + Log.d(TAG, "Delete local category : $localCategory") + } + } + + val remoteCurrencies = projResponse.getCurrencies(project.id) + val remoteCurrenciesByRemoteId = remoteCurrencies.associateBy { it.remoteId } + + for (c in remoteCurrencies) { + val localCurrency = dbHelper.getCurrency(c.remoteId, project.id) + if (localCurrency == null) { + Log.d(TAG, "Add local currency : $c") + dbHelper.addCurrency(c) + } else { + if (c.name == localCurrency.name && + c.exchangeRate == localCurrency.exchangeRate + ) { + Log.d(TAG, "Nothing to do for currency : $localCurrency") + } else { + Log.d(TAG, "Update local currency : $c") + dbHelper.updateCurrency(localCurrency.id, c.name, c.exchangeRate) + } + } + } + + val localCurrencies = dbHelper.getCurrencies(project.id) + for (localCurrency in localCurrencies) { + if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) { + dbHelper.deleteCurrency(localCurrency.id) + Log.d(TAG, "Delete local currency : $localCurrencies") + } + } + + val remoteMembers = projResponse.getMembers(project.id) + val remoteMembersByRemoteId = remoteMembers.associateBy { it.remoteId } + + for (m in remoteMembers) { + val localMember = dbHelper.getMember(m.remoteId, project.id) + if (localMember == null) { + Log.d(TAG, "Add local member : $m") + val mid = dbHelper.addMember(m) + if (!m.ncUserId.isNullOrEmpty()) { + updateMemberAvatar(mid) + } + } else { + val ncUserIdChanged = ( + (m.ncUserId == null && localMember.ncUserId != null) || + (m.ncUserId != null && localMember.ncUserId == null) || + (m.ncUserId != null && m.ncUserId != localMember.ncUserId) + ) + Log.e("PULLREMOTE", "member NC user id : ${localMember.ncUserId} => ${m.ncUserId} ID changed $ncUserIdChanged") + if (ncUserIdChanged && m.ncUserId == null) { + m.ncUserId = "" + } + if (m.name == localMember.name && + m.weight == localMember.weight && + m.isActivated == localMember.isActivated && + ((m.r == null && m.g == null && m.b == null) || + (m.r == localMember.r && m.g == localMember.g && m.b == localMember.b)) && + !ncUserIdChanged + ) { + Log.d(TAG, "Nothing to do for member : $localMember") + if (!localMember.ncUserId.isNullOrEmpty() && localMember.avatar.isNullOrEmpty()) { + Log.d(TAG, "except updating avatar") + updateMemberAvatar(localMember.id) + } + } else { + Log.d(TAG, "Update local member : $m") + var r = m.r + var g = m.g + var b = m.b + if (m.r == null && m.g == null && m.b == null) { + r = localMember.r + g = localMember.g + b = localMember.b + } + val needAvatarUpdate = (ncUserIdChanged && !m.ncUserId.isNullOrEmpty()) + val newAvatar = if (ncUserIdChanged) "" else null + dbHelper.updateMember( + localMember.id, m.name, m.weight, + m.isActivated, null, null, + r, g, b, m.ncUserId, newAvatar + ) + if (needAvatarUpdate) { + Log.e("PLOP", "pullremote : update member avatar") + updateMemberAvatar(localMember.id) + } + } + } + } + + val dbMembers = dbHelper.getMembersOfProject(project.id, null) + val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id } + + val billsResponse = client!!.getBills(project) + val isIHM = project.type == ProjectType.IHATEMONEY + val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp + val remoteBills: List = if (isIHM) { + billsResponse.getBillsIHM(project.id, memberRemoteIdToId) + } else { + billsResponse.getBillsCospend(project.id, memberRemoteIdToId) + } + val remoteAllBillIds: List = if (isIHM) { + remoteBills.map { it.remoteId } + } else { + billsResponse.allBillIds + } + + val remoteBillsByRemoteId = remoteBills.associateBy { it.remoteId } + val localBills = dbHelper.getBillsOfProject(project.id) + val localBillsByRemoteId = localBills.associateBy { it.remoteId } + + for (remoteBill in remoteBills) { + if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) { + dbHelper.addBill(remoteBill) + nbPulledNewBills++ + newBillsDialogText += "+ ${remoteBill.what}\n" + Log.d(TAG, "Add local bill : $remoteBill") + } else { + val localBill = localBillsByRemoteId[remoteBill.remoteId]!! + if (hasChanged(localBill, remoteBill)) { + dbHelper.updateBill( + localBill.id, null, remoteBill.payerId, + remoteBill.amount, remoteBill.timestamp, + remoteBill.what, DBBill.STATE_OK, remoteBill.repeat, + remoteBill.paymentMode, remoteBill.paymentModeRemoteId, + remoteBill.categoryRemoteId, remoteBill.comment + ) + nbPulledUpdatedBills++ + updatedBillsDialogText += "✏ ${remoteBill.what}\n" + Log.d(TAG, "Update local bill : $remoteBill") + } else { + Log.d(TAG, "Nothing to do for bill : $localBill") + } + + val localBillOwersByIds = localBill.billOwers.associateBy { it.memberId } + val remoteBillOwersByIds = remoteBill.billOwers.associateBy { it.memberId } + + for (rbo in remoteBill.billOwers) { + if (!localBillOwersByIds.containsKey(rbo.memberId)) { + dbHelper.addBillower(localBill.id, rbo.memberId) + Log.d(TAG, "Add local billOwer : $rbo") + } + } + for (lbo in localBill.billOwers) { + if (!remoteBillOwersByIds.containsKey(lbo.memberId)) { + dbHelper.deleteBillOwer(lbo.id) + Log.d(TAG, "Delete local billOwer : $lbo") + } + } + } + } + + if (project.type == ProjectType.COSPEND || project.type == ProjectType.IHATEMONEY) { + for (localBill in localBills) { + if (!remoteAllBillIds.contains(localBill.remoteId)) { + dbHelper.deleteBill(localBill.id) + nbPulledDeletedBills++ + deletedBillsDialogText += "🗑 ${localBill.what}\n" + Log.d(TAG, "Delete local bill : $localBill") + } + } + } else { + for (localBill in localBills) { + if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) { + dbHelper.deleteBill(localBill.id) + nbPulledDeletedBills++ + deletedBillsDialogText += "🗑 ${localBill.what}\n" + Log.d(TAG, "Delete local bill : $localBill") + } + } + } + + val localMembers = dbHelper.getMembersOfProject(project.id, null) + for (localMember in localMembers) { + if (!remoteMembersByRemoteId.containsKey(localMember.remoteId)) { + if (dbHelper.getBillsOfMember(localMember.id).isEmpty() + && dbHelper.getBillowersOfMember(localMember.id).isEmpty() + ) { + dbHelper.deleteMember(localMember.id) + Log.d(TAG, "Delete local member : $localMember") + } else { + Log.d(TAG, "WARNING local member : ${localMember.name} does not exist remotely but is still involved in some bills") + } + } + } + + dbHelper.updateProject( + project.id, null, null, + null, null, serverSyncTimestamp, + null, null, null, + null + ) + LoginStatus.OK + } catch (_: ServerResponse.NotModifiedException) { + Log.d(TAG, "No changes, nothing to do.") + LoginStatus.OK + } catch (e: IOException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + Log.e(TAG, "Catch MISMATCHTOKEN", e) + LoginStatus.SSO_TOKEN_MISMATCH + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "Catch NC REQ failed", e) + errorMessages.add(getErrorMessageFromException(e)) + e.cause?.let { exceptions.add(it) } + LoginStatus.REQ_FAILED + } + } + + private fun onPostExecute(status: LoginStatus) { + if (status != LoginStatus.OK) { + var errorString = "" + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + val obj = SupportUtil.getJsonObject(e.message) + if (obj != null && obj.has("message")) { + try { + errorString += "${obj.getString("message")}\n" + } catch (_: JSONException) { + } + } + } + val intent = Intent(MainConstants.BROADCAST_PROJECT_SYNC_FAILED) + intent.putExtra(MainConstants.BROADCAST_ERROR_MESSAGE, errorString) + intent.putExtra(MainConstants.BROADCAST_PROJECT_ID, project.id) + appContext.sendBroadcast(intent) + if (status == LoginStatus.SSO_TOKEN_MISMATCH) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_SSO_TOKEN_MISMATCH)) + } + } else { + val intent = Intent(MainConstants.BROADCAST_PROJECT_SYNCED) + intent.putExtra(MainConstants.BROADCAST_EXTRA_PARAM, project.name) + appContext.sendBroadcast(intent) + } + syncActive = false + for (callback in callbacks) { + callback.onFinish() + } + if (projectIdsToSync.isNotEmpty()) { + val pid = projectIdsToSync.removeAt(projectIdsToSync.size - 1) + scheduleSync(false, pid) + } + } + } + + fun getErrorMessageFromException(e: NextcloudHttpRequestFailedException): String { + var message = "" + when (e.statusCode) { + 503 -> message += appContext.getString(R.string.error_maintenance_mode) + 400 -> message += appContext.getString(R.string.error_400) + 401 -> message += appContext.getString(R.string.error_401) + 403 -> message += appContext.getString(R.string.error_403) + 404 -> message += appContext.getString(R.string.error_404) + } + message += "\n" + e.cause?.message + return message + } + + private fun createVersatileProjectSyncClient(cospendVersion: String?): VersatileProjectSyncClient? { + val preferences = PreferenceManager.getDefaultSharedPreferences(appContext) + val useSSO = preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false) + return if (useSSO) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(appContext) + val nextcloudAPI = NextcloudAPI(appContext, ssoAccount, GsonBuilder().create(), apiCallback) + VersatileProjectSyncClient("", "", "", nextcloudAPI, ssoAccount, cospendVersion, appContext) + } catch (_: Exception) { + null + } + } else { + val url = preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS) ?: "" + val username = preferences.getString(AccountActivity.SETTINGS_USERNAME, AccountActivity.DEFAULT_SETTINGS) ?: "" + val password = preferences.getString(AccountActivity.SETTINGS_PASSWORD, AccountActivity.DEFAULT_SETTINGS) ?: "" + VersatileProjectSyncClient(url, username, password, null, null, cospendVersion, appContext) + } + } + + fun canCreateAuthenticatedProject(project: DBProject): Boolean { + val isCospend = ProjectType.COSPEND == project.type + val projUrl = project.serverUrl?.replace("/index.php/apps/cospend", "")?.replace("/+$".toRegex(), "") ?: "" + + val accountUrl = if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(appContext) + ssoAccount.url.replace("/+$".toRegex(), "") + } catch (_: Exception) { + return false + } + } else { + preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS)?.replace("/$".toRegex(), "") ?: "" + } + + Log.v(TAG, "proj url : $projUrl ; account url : $accountUrl") + return isCospend && projUrl == accountUrl + } + + fun editRemoteProject(projId: Long, newName: String?, newEmail: String?, + newPassword: String?, newMainCurrencyName: String?, callback: ICallback): Boolean { + updateNetworkStatus() + if (isSyncPossible) { + EditRemoteProjectTask(projId, newName, newEmail, newPassword, newMainCurrencyName, callback).execute() + return true + } + return false + } + + private inner class EditRemoteProjectTask( + projId: Long, + private val newName: String?, + private val newEmail: String?, + private val newPassword: String?, + private val newMainCurrencyName: String?, + private val callback: ICallback + ) { + private val project: DBProject? = dbHelper.getProject(projId) + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + + fun execute(): EditRemoteProjectTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + val nextcloudClient = createNextcloudClient() + var version: String? = null + if (nextcloudClient != null) { + try { + val response = nextcloudClient.getCapabilities(null) + version = response.cospendVersion + } catch (e: Exception) { + Log.i(TAG, "Failed to get cospend version when syncing: $e") + } + } + val client = createVersatileProjectSyncClient(version) + if (BillsListViewActivity.DEBUG) { + Log.i(TAG, "STARTING edit remote project") + } + var status = LoginStatus.OK + try { + val response = client!!.editRemoteProject( + project!!, newName, newEmail, newPassword, newMainCurrencyName + ) + if (BillsListViewActivity.DEBUG) { + Log.i(TAG, "RESPONSE edit remote project : ${response.stringContent}") + } + } catch (e: IOException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception", e) + exceptions.add(e) + status = LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(TAG, "Catch JSON exception", e) + status = LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + Log.e(TAG, "Catch MISMATCHTOKEN", e) + status = LoginStatus.SSO_TOKEN_MISMATCH + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "Catch NC REQ failed", e) + status = LoginStatus.REQ_FAILED + errorMessages.add(getErrorMessageFromException(e)) + } + if (BillsListViewActivity.DEBUG) Log.i(TAG, "FINISHED edit remote project") + return status + } + + private fun onPostExecute(status: LoginStatus) { + var errorString = "" + if (status != LoginStatus.OK) { + errorString = appContext.getString(R.string.error_sync, appContext.getString(status.str)) + "\n\n" + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + errorString += "${e.javaClass.name}: ${e.message}" + } + if (status == LoginStatus.SSO_TOKEN_MISMATCH) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_SSO_TOKEN_MISMATCH)) + } + } else { + dbHelper.updateProject( + project!!.id, newName, newEmail, newPassword, + null, null, null, null, null, null + ) + } + callback.onFinish(newName ?: "", errorString) + } + } + + fun deleteRemoteProject(projId: Long, callback: ICallback): Boolean { + updateNetworkStatus() + if (isSyncPossible) { + DeleteRemoteProjectTask(projId, callback).execute() + return true + } + return false + } + + private inner class DeleteRemoteProjectTask(projId: Long, private val callback: ICallback) { + private val project: DBProject? = dbHelper.getProject(projId) + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + + fun execute(): DeleteRemoteProjectTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + val nextcloudClient = createNextcloudClient() + var version: String? = null + if (nextcloudClient != null) { + try { + val response = nextcloudClient.getCapabilities(null) + version = response.cospendVersion + } catch (e: Exception) { + Log.i(TAG, "Failed to get cospend version when syncing: $e") + } + } + val client = createVersatileProjectSyncClient(version) + if (BillsListViewActivity.DEBUG) Log.i(TAG, "STARTING delete remote project") + var status = LoginStatus.OK + try { + val response = client!!.deleteRemoteProject(project!!) + if (BillsListViewActivity.DEBUG) Log.i(TAG, "RESPONSE delete remote project : ${response.stringContent}") + } catch (e: IOException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception", e) + exceptions.add(e) + status = LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(TAG, "Catch JSONException", e) + status = LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + Log.e(TAG, "Catch MISMATCHTOKEN", e) + status = LoginStatus.SSO_TOKEN_MISMATCH + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "Catch NC REQ failed", e) + status = LoginStatus.REQ_FAILED + errorMessages.add(getErrorMessageFromException(e)) + } + if (BillsListViewActivity.DEBUG) Log.i(TAG, "FINISHED delete device") + return status + } + + private fun onPostExecute(status: LoginStatus) { + var errorString = "" + if (status != LoginStatus.OK) { + errorString = appContext.getString(R.string.error_sync, appContext.getString(status.str)) + "\n\n" + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + errorString += "${e.javaClass.name}: ${e.message}" + } + if (status == LoginStatus.SSO_TOKEN_MISMATCH) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_SSO_TOKEN_MISMATCH)) + } + } else { + dbHelper.deleteProject(project!!.id) + } + callback.onFinish(project?.id?.toString() ?: "", errorString) + } + } + + fun createRemoteProject(remoteId: String, name: String, email: String, password: String, ihmUrl: String, projectType: ProjectType, callback: IProjectCreationCallback): Boolean { + if (isSyncPossible) { + val proj = DBProject( + 0, remoteId, + if (projectType == ProjectType.COSPEND) "" else password, + name, ihmUrl, email, + null, projectType, 0L, null, + false, DBProject.ACCESS_LEVEL_UNKNOWN, null + ) + CreateRemoteProjectTask(proj, callback).execute() + return true + } + return false + } + + private inner class CreateRemoteProjectTask(private val project: DBProject, private val callback: IProjectCreationCallback) { + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + private var usePrivateApi = false + + fun execute(): CreateRemoteProjectTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + val nextcloudClient = createNextcloudClient() + var version: String? = null + if (nextcloudClient != null) { + try { + val response = nextcloudClient.getCapabilities(null) + version = response.cospendVersion + } catch (e: Exception) { + Log.i(TAG, "Failed to get cospend version when syncing: $e") + } + } + val client = createVersatileProjectSyncClient(version) + if (BillsListViewActivity.DEBUG) Log.i(TAG, "STARTING create remote project") + var status = LoginStatus.OK + try { + val response: ServerResponse.CreateRemoteProjectResponse + if (canCreateAuthenticatedProject(project)) { + response = client!!.createAuthenticatedRemoteProject(project) + usePrivateApi = true + } else { + response = client!!.createAnonymousRemoteProject(project) + } + if (BillsListViewActivity.DEBUG) Log.i(TAG, "RESPONSE create remote project : ${response.stringContent}") + } catch (e: IOException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception", e) + exceptions.add(e) + status = LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "JSON Exception", e) + exceptions.add(e) + status = LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception", e) + exceptions.add(e) + status = LoginStatus.CONNECTION_FAILED + } catch (e: NextcloudHttpRequestFailedException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception", e) + exceptions.add(e) + status = LoginStatus.REQ_FAILED + errorMessages.add(getErrorMessageFromException(e)) + } + if (BillsListViewActivity.DEBUG) Log.i(TAG, "FINISHED create remote project") + return status + } + + private fun onPostExecute(status: LoginStatus) { + var errorString = "" + if (status != LoginStatus.OK) { + errorString = appContext.getString(R.string.error_sync, appContext.getString(status.str)) + "\n\n" + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + errorString += "${e.javaClass.name}: ${e.message}" + } + } + callback.onFinish(project.remoteId, errorString, usePrivateApi) + } + } + + private fun hasChanged(localBill: DBBill, remoteBill: DBBill): Boolean { + if (localBill.payerId == remoteBill.payerId && + localBill.amount == remoteBill.amount && + localBill.timestamp == remoteBill.timestamp && + localBill.what == remoteBill.what && + localBill.comment == remoteBill.comment && + localBill.paymentMode == remoteBill.paymentMode && + localBill.paymentModeRemoteId == remoteBill.paymentModeRemoteId && + localBill.categoryRemoteId == remoteBill.categoryRemoteId + ) { + val localRepeat = localBill.repeat ?: DBBill.NON_REPEATED + val remoteRepeat = remoteBill.repeat ?: DBBill.NON_REPEATED + return localRepeat != remoteRepeat + } + return true + } + + fun updateMemberAvatar(memberId: Long) { + updateNetworkStatus() + if (isNextcloudAccountConfigured(appContext) && isSyncPossible) { + UpdateMemberAvatarTask(memberId).execute() + } + } + + fun runAccountProjectsSync() { + Log.d(TAG, "Account projects sync requested; ${if (syncAccountProjectsActive) "sync active" else "sync NOT active"}) ...") + updateNetworkStatus() + if (isNextcloudAccountConfigured(appContext) && isSyncPossible && !syncAccountProjectsActive) { + SyncAccountProjectsTask().execute() + if (preferences.getBoolean(appContext.getString(R.string.pref_key_use_server_color), true)) { + GetNCColorTask().execute() + } + GetNCUserAvatarTask().execute() + } + } + + private val apiCallback = object : NextcloudAPI.ApiConnectedListener { + override fun onConnected() { + Log.d(TAG, "API connected!!!!") + } + + override fun onError(ex: Exception) {} + } + + private fun createNextcloudClient(): NextcloudClient? { + val useSSO = preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false) + return if (useSSO) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(appContext) + val nextcloudAPI = NextcloudAPI(appContext, ssoAccount, GsonBuilder().create(), apiCallback) + NextcloudClient("", ssoAccount.userId, "", nextcloudAPI, appContext) + } catch (_: Exception) { + null + } + } else { + val url = preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS) ?: "" + val username = preferences.getString(AccountActivity.SETTINGS_USERNAME, AccountActivity.DEFAULT_SETTINGS) ?: "" + val password = preferences.getString(AccountActivity.SETTINGS_PASSWORD, AccountActivity.DEFAULT_SETTINGS) ?: "" + NextcloudClient(url, username, password, null, appContext) + } + } + + private inner class SyncAccountProjectsTask { + private var client: NextcloudClient? = null + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + + fun execute(): SyncAccountProjectsTask { + scope.launch { + syncAccountProjectsActive = true + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + syncAccountProjectsActive = false + } + return this + } + + private fun doWork(): LoginStatus { + client = createNextcloudClient() + Log.i(TAG, "STARTING account projects SYNCHRONIZATION") + val status = client?.let { pullRemoteProjects(it) } ?: LoginStatus.SSO_TOKEN_MISMATCH + Log.i(TAG, "SYNCHRONIZATION FINISHED") + return status + } + + private fun pullRemoteProjects(client: NextcloudClient): LoginStatus { + Log.d(TAG, "pullRemoteProjects()") + return try { + val url = if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + SingleAccountHelper.getCurrentSingleSignOnAccount(appContext).url + } else { + preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS) ?: "" + } + + val localProjects = dbHelper.projects + val capabilitiesResponse = client.getCapabilities(null) + val cospendVersion = capabilitiesResponse.cospendVersion + val useOcsApi = cospendVersion != null && SupportUtil.compareVersions(cospendVersion, "1.6.1") >= 0 + + val response = client.getAccountProjects(useOcsApi) + val remoteAccountProjects = response.getAccountProjects(url) + dbHelper.clearAccountProjects() + for (remoteAccountProject in remoteAccountProjects) { + dbHelper.addAccountProject(remoteAccountProject) + Log.v(TAG, "received account project $remoteAccountProject") + val existingProj = localProjects.find { + it.remoteId == remoteAccountProject.remoteId && + it.serverUrl?.replace("/+$".toRegex(), "") == remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend" + } + if (existingProj == null) { + val newProj = DBProject(0, + remoteAccountProject.remoteId, + "", + remoteAccountProject.name, + remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend", + "", + null, + ProjectType.COSPEND, + 0L, + null, + false, + DBProject.ACCESS_LEVEL_UNKNOWN, + null, + remoteAccountProject.archivedTs + ) + dbHelper.addProject(newProj) + } else if (existingProj.archivedTs != remoteAccountProject.archivedTs) { + dbHelper.updateProject( + projId = existingProj.id, + newName = null, + newEmail = null, + newPassword = null, + newLastPayerId = null, + newLastSyncedTimestamp = null, + newCurrencyName = null, + newDeletionDisabled = null, + newMyAccessLevel = null, + newBearerToken = null, + newArchivedTs = remoteAccountProject.archivedTs ?: 0L + ) + } + } + LoginStatus.OK + } catch (_: ServerResponse.NotModifiedException) { + Log.d(TAG, "No changes, nothing to do.") + LoginStatus.OK + } catch (e: IOException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(TAG, "Exception", e) + exceptions.add(e) + LoginStatus.JSON_FAILED + } catch (e: TokenMismatchException) { + Log.e(TAG, "Catch MISMATCHTOKEN", e) + LoginStatus.SSO_TOKEN_MISMATCH + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "Catch REQ FAILED", e) + errorMessages.add(getErrorMessageFromException(e)) + LoginStatus.REQ_FAILED + } + } + + private fun onPostExecute(status: LoginStatus) { + if (status != LoginStatus.OK) { + var errorString = appContext.getString(R.string.error_sync, appContext.getString(status.str)) + "\n\n" + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + errorString += "${e.javaClass.name}: ${e.message}" + } + val intent = Intent(MainConstants.BROADCAST_ACCOUNT_PROJECTS_SYNC_FAILED) + intent.putExtra(MainConstants.BROADCAST_ERROR_MESSAGE, errorString) + appContext.sendBroadcast(intent) + if (status == LoginStatus.SSO_TOKEN_MISMATCH) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_SSO_TOKEN_MISMATCH)) + } + } else { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_ACCOUNT_PROJECTS_SYNCED)) + } + } + } + + private inner class GetNCColorTask { + fun execute(): GetNCColorTask { + scope.launch { + withContext(Dispatchers.IO) { + doWork() + } + } + return this + } + + private fun doWork(): LoginStatus { + val client = createNextcloudClient() + Log.i(TAG, "STARTING get color") + return if (client != null) { + getNextcloudColor(client) + } else { + LoginStatus.SSO_TOKEN_MISMATCH + } + } + + private fun getNextcloudColor(client: NextcloudClient): LoginStatus { + Log.d(TAG, "getNextcloudColor()") + return try { + val response = client.getCapabilities(null) + var color = response.color + + if (!color.isNullOrEmpty() && color.startsWith("#")) { + if (color.length == 4) { + color = "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3] + } + val intColor = color.toColorInt() + Log.d(TAG, "COLOR from server is $color") + preferences.edit { + putInt( + appContext.getString(R.string.pref_key_server_color), + intColor + ) + } + } + LoginStatus.OK + } catch (e: Exception) { + Log.e(TAG, "Exception in get color", e) + LoginStatus.CONNECTION_FAILED + } + } + } + + private inner class GetNCUserAvatarTask { + fun execute(): GetNCUserAvatarTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + val client = createNextcloudClient() + Log.i(TAG, "STARTING get account avatar") + return if (client != null) { + getNextcloudUserAvatar(client) + } else { + LoginStatus.SSO_TOKEN_MISMATCH + } + } + + private fun getNextcloudUserAvatar(client: NextcloudClient): LoginStatus { + Log.d(TAG, "getNextcloudUserAvatar()") + return try { + val response = client.getAvatar(null) + val avatar = response.avatarString + + if (avatar.isNotEmpty()) { + preferences.edit { + putString( + appContext.getString(R.string.pref_key_avatar), + avatar + ) + } + } + LoginStatus.OK + } catch (e: Exception) { + Log.e(TAG, "Exception in get avatar", e) + LoginStatus.CONNECTION_FAILED + } + } + + private fun onPostExecute(status: LoginStatus) { + if (status == LoginStatus.OK) { + appContext.sendBroadcast(Intent(MainConstants.BROADCAST_AVATAR_UPDATED)) + } + } + } + + private inner class UpdateMemberAvatarTask(private val memberId: Long) { + fun execute(): UpdateMemberAvatarTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + val client = createNextcloudClient() + Log.i(TAG, "STARTING get avatar for member") + return if (client != null) { + getNextcloudUserAvatar(client) + } else { + LoginStatus.SSO_TOKEN_MISMATCH + } + } + + private fun getNextcloudUserAvatar(client: NextcloudClient): LoginStatus { + Log.d(TAG, "getNextcloudUserAvatar() $memberId") + return try { + val m = dbHelper.getMember(memberId) + val targetUserName = m?.ncUserId + if (!targetUserName.isNullOrEmpty()) { + val response = client.getAvatar(targetUserName) + val avatar = response.avatarString + + if (avatar.isNotEmpty()) { + dbHelper.updateMember( + memberId, null, null, null, + null, null, null, null, null, + null, avatar + ) + Log.d(TAG, "RECEIVED AVATAR for member $memberId length ${avatar.length}") + } + } + LoginStatus.OK + } catch (e: Exception) { + Log.e(TAG, "Exception in get member avatar", e) + LoginStatus.CONNECTION_FAILED + } + } + + private fun onPostExecute(status: LoginStatus) { + if (status == LoginStatus.OK) { + val intent = Intent(MainConstants.BROADCAST_AVATAR_UPDATED) + intent.putExtra(MainConstants.BROADCAST_AVATAR_UPDATED_MEMBER, memberId) + appContext.sendBroadcast(intent) + } + } + } + + fun getRemoteProjectInfo(project: DBProject, callback: ICallback): Boolean { + if (isSyncPossible) { + GetRemoteProjectInfoTask(project, callback).execute() + return true + } + return false + } + + private inner class GetRemoteProjectInfoTask(private val project: DBProject, private val callback: ICallback) { + private val exceptions: MutableList = ArrayList() + private val errorMessages: MutableList = ArrayList() + + fun execute(): GetRemoteProjectInfoTask { + scope.launch { + val status = withContext(Dispatchers.IO) { + doWork() + } + onPostExecute(status) + } + return this + } + + private fun doWork(): LoginStatus { + var version: String? = null + if (project.type == ProjectType.COSPEND) { + val nextcloudClient = createNextcloudClient() + if (nextcloudClient != null) { + try { + val response = nextcloudClient.getCapabilities(project) + version = response.cospendVersion + } catch (e: Exception) { + Log.i(TAG, "Failed to get cospend version when syncing: $e") + } + } + } + val client = createVersatileProjectSyncClient(version) + if (BillsListViewActivity.DEBUG) Log.i(TAG, "STARTING create remote project") + return try { + val projResponse = client!!.getProject(project, 0, null) + val name = projResponse.name + val email = projResponse.email + Log.e(TAG, "Project info: $name and $email") + LoginStatus.OK + } catch (e: NextcloudHttpRequestFailedException) { + if (BillsListViewActivity.DEBUG) Log.e(TAG, "Exception1", e) + errorMessages.add(getErrorMessageFromException(e)) + LoginStatus.REQ_FAILED + } catch (e: Exception) { + Log.e(TAG, "Failed to get project info: $e") + exceptions.add(e) + LoginStatus.CONNECTION_FAILED + } + } + + private fun onPostExecute(status: LoginStatus) { + var errorString = "" + if (status != LoginStatus.OK) { + for (errorMessage in errorMessages) { + errorString += "$errorMessage\n" + } + errorString += "\n" + for (e in exceptions) { + errorString += "${e.javaClass.name}: ${e.message}" + } + } + callback.onFinish("", errorString) + } + } + + companion object { + private val TAG = CowspentServerSyncHelper::class.java.simpleName + + private var instance: CowspentServerSyncHelper? = null + private val projectIdsToSync: MutableList = ArrayList() + + @Synchronized + fun getInstance(dbHelper: CowspentSQLiteOpenHelper): CowspentServerSyncHelper { + if (instance == null) { + instance = CowspentServerSyncHelper(dbHelper) + } + return instance!! + } + + fun isNextcloudAccountConfigured(context: Context): Boolean { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + return !preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS).isNullOrEmpty() || + preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false) + } + + fun getNextcloudAccountServerUrl(context: Context): String { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + return if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context.applicationContext) + ssoAccount.url.replace("/+$".toRegex(), "") + } catch (_: Exception) { + "" + } + } else { + preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS)?.replace("/+$".toRegex(), "") ?: "" + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt b/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt new file mode 100644 index 0000000..dfdcb6c --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt @@ -0,0 +1,119 @@ +package net.helcel.cowspent.theme + +import android.annotation.SuppressLint +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.preference.PreferenceManager +import net.helcel.cowspent.util.ColorUtils + +object ThemeUtils { + + val Shapes = Shapes( + small = RoundedCornerShape(12.dp), + medium = RoundedCornerShape(24.dp), + large = RoundedCornerShape(28.dp) + ) + + @SuppressLint("ConflictingOnColor") + @Composable + fun CowspentTheme( + accentColor: Int? = null, + darkTheme: Boolean? = null, + content: @Composable () -> Unit + ) { + val context = LocalContext.current + val config = LocalConfiguration.current + val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } + val nightModeKey = stringResource(net.helcel.cowspent.R.string.pref_key_night_mode) + + val resolvedDarkTheme = darkTheme ?: run { + val nightMode = sharedPreferences.getString(nightModeKey, "-1") ?: "-1" + when (nightMode) { + "1" -> false + "2" -> true + else -> isSystemInDarkTheme() + } + } + + val resolvedAccentColor = accentColor ?: remember(resolvedDarkTheme) { + ColorUtils.primaryColor(context, resolvedDarkTheme) + } + + val themedContext = remember(resolvedDarkTheme) { + + if (resolvedDarkTheme != (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)) { + config.uiMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or + if (resolvedDarkTheme) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO + context.createConfigurationContext(config) + } else { + context + } + } + + val onPrimary = if (ColorUtils.isLightColor(resolvedAccentColor)) Color.Black else Color.White + + val colors = if (resolvedDarkTheme) { + darkColors( + primary = Color(resolvedAccentColor), + primaryVariant = Color(resolvedAccentColor), + onPrimary = onPrimary, + secondary = Color(resolvedAccentColor), + onSecondary = onPrimary, + background = Color(0xFF121212), + surface = Color(0xFF121212), + onBackground = Color.White, + onSurface = Color.White + ) + } else { + lightColors( + primary = Color(resolvedAccentColor), + primaryVariant = Color(resolvedAccentColor), + onPrimary = onPrimary, + secondary = Color(resolvedAccentColor), + onSecondary = onPrimary, + background = Color.White, + surface = Color.White, + onBackground = Color.Black, + onSurface = Color.Black + ) + } + + MaterialTheme( + colors = colors, + shapes = Shapes, + content = { + CompositionLocalProvider(LocalContext provides themedContext) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.primary) + .statusBarsPadding() + ) + Box(modifier = Modifier.fillMaxSize()) { + content() + } + } + } + } + } + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/BillFormatter.kt b/app/src/main/java/net/helcel/cowspent/util/BillFormatter.kt new file mode 100644 index 0000000..3af97c3 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/BillFormatter.kt @@ -0,0 +1,58 @@ +package net.helcel.cowspent.util + +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBPaymentMode + +object BillFormatter { + fun formatBills( + bills: List, + membersMap: Map, + categoriesMap: Map, + paymentModesMap: Map + ) { + for (bill in bills) { + var whatPrefix = "" + val pm = paymentModesMap[bill.paymentModeRemoteId.toLong()] + if (pm != null) { + whatPrefix += pm.icon + " " + } else { + when (bill.paymentModeRemoteId) { + DBBill.PAYMODE_ID_CARD -> whatPrefix += "\uD83D\uDCB3 " + DBBill.PAYMODE_ID_CASH -> whatPrefix += "\uD83D\uDCB5 " + DBBill.PAYMODE_ID_CHECK -> whatPrefix += "\uD83C\uDFAB " + DBBill.PAYMODE_ID_TRANSFER -> whatPrefix += "⇄ " + DBBill.PAYMODE_ID_ONLINE_SERVICE -> whatPrefix += "\uD83C\uDF0E " + } + } + + val cat = categoriesMap[bill.categoryRemoteId.toLong()] + if (cat != null) { + whatPrefix += cat.icon + " " + } else { + when (bill.categoryRemoteId) { + DBBill.CATEGORY_GROCERIES -> whatPrefix += "\uD83D\uDED2 " + DBBill.CATEGORY_LEISURE -> whatPrefix += "\uD83C\uDF89 " + DBBill.CATEGORY_RENT -> whatPrefix += "\uD83C\uDFE0 " + DBBill.CATEGORY_BILLS -> whatPrefix += "\uD83C\uDF29 " + DBBill.CATEGORY_CULTURE -> whatPrefix += "\uD83D\uDEB8 " + DBBill.CATEGORY_HEALTH -> whatPrefix += "\uD83D\uDC9A " + DBBill.CATEGORY_SHOPPING -> whatPrefix += "\uD83D\uDECD " + DBBill.CATEGORY_REIMBURSEMENT -> whatPrefix += "\uD83D\uDCB0 " + DBBill.CATEGORY_RESTAURANT -> whatPrefix += "\uD83C\uDF74 " + DBBill.CATEGORY_ACCOMMODATION -> whatPrefix += "\uD83D\uDECC " + DBBill.CATEGORY_TRANSPORT -> whatPrefix += "\uD83D\uDE8C " + DBBill.CATEGORY_SPORT -> whatPrefix += "\uD83C\uDFBE " + } + } + bill.formattedWhat = whatPrefix + bill.what + + val payerName = membersMap[bill.payerId]?.name ?: bill.payerId.toString() + val owersNames = bill.billOwersIds.joinToString(", ") { id -> + membersMap[id]?.name ?: id.toString() + } + bill.formattedSubtitle = "$payerName \u2192 $owersNames" + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/BillParser.kt b/app/src/main/java/net/helcel/cowspent/util/BillParser.kt new file mode 100644 index 0000000..f403b78 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/BillParser.kt @@ -0,0 +1,68 @@ +package net.helcel.cowspent.util + +import net.helcel.cowspent.model.parsed.AustrianBillQrCode +import net.helcel.cowspent.model.parsed.CroatianBillQrCode +import java.text.ParseException +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import kotlin.math.round +import androidx.core.net.toUri + +object BillParser { + private val austrianQrCodeDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT) + private val croatianQrCodeDateFormat = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm") + + @Throws(ParseException::class) + fun parseAustrianBillFromQrCode(scannedBill: String): AustrianBillQrCode { + val splitBill = scannedBill.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + + if (splitBill.size < 10) { + throw ParseException("Could not parse bill to Austrian format!", 0) + } + + val date = austrianQrCodeDateFormat.parse(splitBill[4]) ?: throw ParseException("Could not parse date", 0) + var totalAmount = 0.0 + for (i in 1..5) { + totalAmount += SupportUtil.commaNumberFormat.parse(splitBill[4 + i])?.toDouble() ?: 0.0 + } + // some amounts may be negative that's why we have to round here + return AustrianBillQrCode(splitBill[2], date, round(totalAmount * 100.0) / 100.0) + } + + @Throws(ParseException::class) + fun parseCroatianBillFromQrCode(scannedBill: String): CroatianBillQrCode { + val uri = scannedBill.toUri() + + // Be defensive, and only allow the known host + if (uri.host == null || uri.host != "porezna.gov.hr") { + throw ParseException("Does not look like a Croatian QR code", 0) + } + + val dates = uri.getQueryParameters("datv") + val amounts = uri.getQueryParameters("izn") + + var date: LocalDateTime? = null + if (!dates.isEmpty()) { + date = LocalDateTime.parse(dates[0], croatianQrCodeDateFormat) + } + + var amount: Double? = null + if (!amounts.isEmpty()) { + try { + amount = SupportUtil.commaNumberFormat.parse(amounts[0])?.toDouble() + } catch (_: NullPointerException) { + // failed to parse as double + } + } + + if (date == null && amount == null) { + throw ParseException("Could not parse bill to Croatian format!", 0) + } + return CroatianBillQrCode( + date, + amount ?: 0.0 + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt b/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt new file mode 100644 index 0000000..7711be2 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt @@ -0,0 +1,37 @@ +package net.helcel.cowspent.util + +import android.content.Context +import net.helcel.cowspent.R +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.model.DBPaymentMode + +object CategoryUtils { + + fun getDefaultCategories(context: Context, projectId: Long): List { + return listOf( + DBCategory(0, DBBill.CATEGORY_GROCERIES.toLong(), projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"), + DBCategory(0, DBBill.CATEGORY_LEISURE.toLong(), projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"), + DBCategory(0, DBBill.CATEGORY_RENT.toLong(), projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"), + DBCategory(0, DBBill.CATEGORY_BILLS.toLong(), projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"), + DBCategory(0, DBBill.CATEGORY_CULTURE.toLong(), projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"), + DBCategory(0, DBBill.CATEGORY_HEALTH.toLong(), projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"), + DBCategory(0, DBBill.CATEGORY_SHOPPING.toLong(), projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"), + DBCategory(0, DBBill.CATEGORY_REIMBURSEMENT.toLong(), projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"), + DBCategory(0, DBBill.CATEGORY_RESTAURANT.toLong(), projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"), + DBCategory(0, DBBill.CATEGORY_ACCOMMODATION.toLong(), projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"), + DBCategory(0, DBBill.CATEGORY_TRANSPORT.toLong(), projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"), + DBCategory(0, DBBill.CATEGORY_SPORT.toLong(), projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177") + ) + } + + fun getDefaultPaymentModes(context: Context, projectId: Long): List { + return listOf( + DBPaymentMode(0, DBBill.PAYMODE_ID_CARD.toLong(), projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"), + DBPaymentMode(0, DBBill.PAYMODE_ID_CASH.toLong(), projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"), + DBPaymentMode(0, DBBill.PAYMODE_ID_CHECK.toLong(), projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"), + DBPaymentMode(0, DBBill.PAYMODE_ID_TRANSFER.toLong(), projectId, context.getString(R.string.payment_mode_transfer), "⇄", "#00ced1"), + DBPaymentMode(0, DBBill.PAYMODE_ID_ONLINE_SERVICE.toLong(), projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc") + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/ColorUtils.kt b/app/src/main/java/net/helcel/cowspent/util/ColorUtils.kt new file mode 100644 index 0000000..0d965ef --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/ColorUtils.kt @@ -0,0 +1,95 @@ +package net.helcel.cowspent.util + +import android.content.Context +import android.content.res.Configuration +import android.graphics.* +import android.os.Build +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException +import net.helcel.cowspent.R +import androidx.core.graphics.createBitmap + +object ColorUtils { + + fun primaryColor(context: Context, isDark: Boolean? = null): Int { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val resolvedIsDark = isDark ?: ((context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) + + // Determine color mode, migrating from old boolean flags if necessary + val modeKey = context.getString(R.string.pref_key_color_mode) + val colorMode = if (prefs.contains(modeKey)) { + prefs.getString(modeKey, "system") + } else { + val useServer = prefs.getBoolean(context.getString(R.string.pref_key_use_server_color), true) + val useSystem = prefs.getBoolean(context.getString(R.string.pref_key_use_system_color), true) + when { + useServer -> "server" + useSystem -> "system" + else -> "manual" + } + } + + if (colorMode == "server") { + val serverColor = prefs.getInt(context.getString(R.string.pref_key_server_color), -1) + if (serverColor != -1) { + return serverColor + } + } + + if (colorMode == "system" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return if (resolvedIsDark) { + context.getColor(android.R.color.system_accent1_200) + } else { + context.getColor(android.R.color.system_accent1_600) + } + } + + val themedContext = if (isDark != null) { + val config = Configuration(context.resources.configuration) + config.uiMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or + if (isDark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO + context.createConfigurationContext(config) + } else { + context + } + + return prefs.getInt( + context.getString(R.string.pref_key_color), + ContextCompat.getColor(themedContext, R.color.primary) + ) + } + + fun isLightColor(color: Int): Boolean { + return androidx.core.graphics.ColorUtils.calculateLuminance(color) > 0.5 + } + + @Throws(WriterException::class) + fun encodeAsBitmap(str: String): Bitmap? { + val result = try { + MultiFormatWriter().encode( + str, + BarcodeFormat.QR_CODE, 400, 400, null + ) + } catch (_: IllegalArgumentException) { + // Unsupported format + return null + } + val w = result.width + val h = result.height + val pixels = IntArray(w * h) + for (y in 0 until h) { + val offset = y * w + for (x in 0 until w) { + pixels[offset + x] = if (result[x, y]) Color.BLACK else Color.WHITE + } + } + val bitmap = createBitmap(w, h) + bitmap.setPixels(pixels, 0, 400, 0, 0, w, h) + return bitmap + } + +} diff --git a/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt b/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt new file mode 100644 index 0000000..dabbdd0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt @@ -0,0 +1,121 @@ +package net.helcel.cowspent.util + +import android.util.Base64 +import android.util.Log +import androidx.annotation.StringRes +import net.helcel.cowspent.R +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.net.MalformedURLException +import java.net.SocketTimeoutException + +object CospendClientUtil { + + enum class LoginStatus(@param:StringRes val str: Int) { + OK(0), + AUTH_FAILED(R.string.error_username_password_invalid), + CONNECTION_FAILED(R.string.error_io), + NO_NETWORK(R.string.error_no_network), + JSON_FAILED(R.string.error_json), + SERVER_FAILED(R.string.error_server), + SSO_TOKEN_MISMATCH(R.string.error_token_mismatch), + REQ_FAILED(R.string.error_req_failed) + } + + fun isHttp(url: String?): Boolean { + return url != null && url.length > 4 && url.startsWith("http") && url[4] != 's' + } + + fun formatURL(urlParam: String): String { + var url = urlParam + if (!url.endsWith("/")) { + url += "/" + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://$url" + } + val replacements = arrayOf("v0.2/", "api/", "apps/", "index.php/") + for (replacement in replacements) { + if (url.endsWith(replacement)) { + url = url.substring(0, url.length - replacement.length) + } + } + return url + } + + fun isValidLogin(url: String, username: String, password: String): LoginStatus { + return try { + val targetURL = url + "index.php/apps/cospend/api/ping" + val con = SupportUtil.getHttpURLConnection(targetURL) + con.requestMethod = "GET" + con.setRequestProperty( + "Authorization", + "Basic " + + String( + Base64.encode( + ("$username:$password").toByteArray(), + Base64.NO_WRAP + ) + ) + ) + con.connectTimeout = 10 * 1000 // 10 seconds + con.connect() + + Log.v(CospendClientUtil::class.java.simpleName, "Establishing connection to server") + when (con.responseCode) { + 200 -> { + Log.v(CospendClientUtil::class.java.simpleName, "" + con.responseMessage) + val result = StringBuilder() + val rd = BufferedReader(InputStreamReader(con.inputStream)) + var line: String? + while ((rd.readLine().also { line = it }) != null) { + result.append(line) + } + Log.v(CospendClientUtil::class.java.simpleName, result.toString()) + JSONArray(result.toString()) + LoginStatus.OK + } + in 401..403 -> { + LoginStatus.AUTH_FAILED + } + else -> { + LoginStatus.SERVER_FAILED + } + } + } catch (e: MalformedURLException) { + Log.e(CospendClientUtil::class.java.simpleName, "Exception", e) + LoginStatus.CONNECTION_FAILED + } catch (e: SocketTimeoutException) { + Log.e(CospendClientUtil::class.java.simpleName, "Exception", e) + LoginStatus.CONNECTION_FAILED + } catch (e: IOException) { + Log.e(CospendClientUtil::class.java.simpleName, "Exception", e) + LoginStatus.CONNECTION_FAILED + } catch (e: JSONException) { + Log.e(CospendClientUtil::class.java.simpleName, "Exception", e) + LoginStatus.JSON_FAILED + } + } + + fun isValidURL(url: String): Boolean { + val result = StringBuilder() + return try { + val con = SupportUtil.getHttpURLConnection( url + "status.php") + con.requestMethod = VersatileProjectSyncClient.METHOD_GET + con.connectTimeout = 10 * 1000 // 10 seconds + val rd = BufferedReader(InputStreamReader(con.inputStream)) + var line: String? + while ((rd.readLine().also { line = it }) != null) { + result.append(line) + } + val response = JSONObject(result.toString()) + response.getBoolean("installed") + } catch (_: Exception) { + false + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/Cowspent.kt b/app/src/main/java/net/helcel/cowspent/util/Cowspent.kt new file mode 100644 index 0000000..833ce5a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/Cowspent.kt @@ -0,0 +1,31 @@ +package net.helcel.cowspent.util + +import android.app.Application +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import net.helcel.cowspent.R + +class Cowspent : Application() { + + override fun onCreate() { + setAppTheme(getAppTheme(applicationContext)) + super.onCreate() + } + + companion object { + + fun setAppTheme(mode: Int) { + AppCompatDelegate.setDefaultNightMode(mode) + } + + fun getAppTheme(context: Context): Int { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val strValue = prefs.getString( + context.getString(R.string.pref_key_night_mode), + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM.toString() + ) + return strValue?.toInt() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/ExportUtil.kt b/app/src/main/java/net/helcel/cowspent/util/ExportUtil.kt new file mode 100644 index 0000000..c3bfec4 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/ExportUtil.kt @@ -0,0 +1,88 @@ +package net.helcel.cowspent.util + +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper + +object ExportUtil { + + @JvmStatic + fun createExportContent(db: CowspentSQLiteOpenHelper, projectId: Long): String { + var fileContent = "" + + // get information + val project = db.getProject(projectId) ?: return "" + val membersById: MutableMap = HashMap() + val members = db.getMembersOfProject(projectId, null) + for (m in members) { + membersById[m.id] = m + } + val bills = db.getBillsOfProject(projectId).toMutableList() + + // write header + fileContent += "what,amount,date,timestamp,payer_name,payer_weight,payer_active,owers,repeat,categoryid,paymentmode\n" + + // write members + for (m in members) { + val fakeBill = DBBill( + 0, 0, projectId, m.id, 1.0, 666, + "deleteMeIfYouWant", DBBill.STATE_OK, DBBill.NON_REPEATED, + DBBill.PAYMODE_NONE, 0, "", 0 + ) + val fakeBillOwers: MutableList = ArrayList() + fakeBillOwers.add(DBBillOwer(0, 0, m.id)) + fakeBill.billOwers = fakeBillOwers + bills.add(0, fakeBill) + } + + // write bills + for (b in bills) { + val payerId = b.payerId + val payer = membersById[payerId] ?: continue + val payerName = payer.name + val payerWeight = payer.weight + val payerActive = if (payer.isActivated) 1 else 0 + val billOwers = b.billOwers + var owersTxt = "" + for (bo in billOwers) { + owersTxt += membersById[bo.memberId]?.name + "," + } + owersTxt = owersTxt.replace(",$".toRegex(), "") + fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," + + "$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryRemoteId}," + + "${b.paymentMode}\n" + } + + // write categories + val cats = db.getCategories(projectId) + if (cats.isNotEmpty()) { + fileContent += "\ncategoryname,categoryid,icon,color\n" + for (cat in cats) { + fileContent += "\"${cat.name}\",${cat.id},\"${cat.icon}\",\"${cat.color}\"\n" + } + } + + // write currencies + val curs = db.getCurrencies(projectId) + if (curs.isNotEmpty() && project.currencyName != null && + project.currencyName!!.isNotEmpty() && project.currencyName != "null" + ) { + fileContent += "\ncurrencyname,exchange_rate\n" + fileContent += "\"${project.currencyName}\",1\n" + for (cur in curs) { + fileContent += "\"${cur.name}\",${cur.exchangeRate}\n" + } + } + + return fileContent + } + + @JvmStatic + fun createExportFileName(db: CowspentSQLiteOpenHelper, projectId: Long): String { + val project = db.getProject(projectId) ?: return "export.csv" + return if (project.name.isEmpty()) { + project.remoteId + ".csv" + } else { + project.name + ".csv" + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/ICallback.kt b/app/src/main/java/net/helcel/cowspent/util/ICallback.kt new file mode 100644 index 0000000..6d4453f --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/ICallback.kt @@ -0,0 +1,10 @@ +package net.helcel.cowspent.util + +/** + * Callback + */ +interface ICallback { + fun onFinish() + fun onFinish(result: String, message: String) + fun onScheduled() +} diff --git a/app/src/main/java/net/helcel/cowspent/util/IProjectCreationCallback.kt b/app/src/main/java/net/helcel/cowspent/util/IProjectCreationCallback.kt new file mode 100644 index 0000000..b70f9c9 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/IProjectCreationCallback.kt @@ -0,0 +1,9 @@ +package net.helcel.cowspent.util + +/** + * Callback + */ +interface IProjectCreationCallback { + fun onFinish(result: String, message: String, usePrivateApi: Boolean) + +} diff --git a/app/src/main/java/net/helcel/cowspent/util/IRefreshBillsListCallback.kt b/app/src/main/java/net/helcel/cowspent/util/IRefreshBillsListCallback.kt new file mode 100644 index 0000000..cbbd216 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/IRefreshBillsListCallback.kt @@ -0,0 +1,8 @@ +package net.helcel.cowspent.util + +/** + * Call back into the BillsListActivity and ask it to refresh the list in the UI + */ +interface IRefreshBillsListCallback { + fun refreshLists(scrollToTop: Boolean) +} diff --git a/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt new file mode 100644 index 0000000..2ac2d64 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt @@ -0,0 +1,302 @@ +package net.helcel.cowspent.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import android.util.Log +import androidx.annotation.WorkerThread +import com.nextcloud.android.sso.QueryParam +import com.nextcloud.android.sso.aidl.NextcloudRequest +import com.nextcloud.android.sso.api.NextcloudAPI +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException +import com.nextcloud.android.sso.exceptions.TokenMismatchException +import net.helcel.cowspent.model.DBProject +import org.json.JSONException +import org.json.JSONObject +import java.io.* +import java.net.HttpURLConnection + +@WorkerThread +class NextcloudClient( + private val url: String, + private val username: String, + private val password: String, + private val nextcloudAPI: NextcloudAPI?, + private val context: Context +) { + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getAccountProjects(useOcsApi: Boolean): ServerResponse.AccountProjectsResponse { + val target = if (useOcsApi) + "/ocs/v2.php/apps/cospend/api/v1/projects" + else + "/index.php/apps/cospend/getProjects" + val method = if (useOcsApi) METHOD_GET else METHOD_POST + return if (nextcloudAPI != null) { + Log.d(javaClass.simpleName, "using SSO to get/sync account projects") + Log.d(javaClass.simpleName, "Sync projects target $target") + ServerResponse.AccountProjectsResponse( + requestServerWithSSO(nextcloudAPI, target, method, null, useOcsApi), + useOcsApi + ) + } else { + Log.d(javaClass.simpleName, "Sync projects target $target") + ServerResponse.AccountProjectsResponse( + requestServer(target, method, null, "", true, useOcsApi), + useOcsApi + ) + } + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getCapabilities(project: DBProject?): ServerResponse.CapabilitiesResponse { + val target: String = if (project == null || url != "") { + "/ocs/v2.php/cloud/capabilities" + } else { + val realServerUrl = project.serverUrl!! + .replace("/apps/cospend", "") + .replace("/index.php", "") + "$realServerUrl/ocs/v2.php/cloud/capabilities" + } + return if (nextcloudAPI != null) { + Log.d(javaClass.simpleName, "using SSO to get color") + ServerResponse.CapabilitiesResponse(requestServerWithSSO(nextcloudAPI, target, METHOD_GET, null, true)) + } else { + ServerResponse.CapabilitiesResponse(requestServer(target, METHOD_GET, null, null, + needLogin = true, + isOCSRequest = true + )) + } + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getAvatar(otherUserName: String?): ServerResponse.AvatarResponse { + val targetUserName = otherUserName ?: username + val target = "/index.php/avatar/$targetUserName/45" + return if (nextcloudAPI != null) { + Log.d(javaClass.simpleName, "using SSO to get avatar") + ServerResponse.AvatarResponse(imageRequestServerWithSSO(nextcloudAPI, target, METHOD_GET, null)) + } else { + ServerResponse.AvatarResponse(imageRequestServer(target, METHOD_GET, null, null, + needLogin = true, + isOCSRequest = false + )) + } + } + + @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + private fun requestServerWithSSO( + nextcloudAPI: NextcloudAPI, + target: String, + method: String, + params: Collection?, + isOCSRequest: Boolean + ): VersatileProjectSyncClient.ResponseData { + val result = StringBuilder() + val headers: MutableMap> = HashMap() + if (isOCSRequest) { + val acceptHeader: MutableList = ArrayList() + acceptHeader.add("application/json") + headers["Accept"] = acceptHeader + } + val nextcloudRequest: NextcloudRequest = if (params == null) { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .setHeader(headers) + .build() + } else { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .setParameter(params) + .setHeader(headers) + .build() + } + try { + val response = nextcloudAPI.performNetworkRequestV2(nextcloudRequest) + val inputStream = response.body + val rd = BufferedReader(InputStreamReader(inputStream)) + var line: String? + while (rd.readLine().also { line = it } != null) { + result.append(line) + } + Log.d(javaClass.simpleName, "RES $result") + inputStream.close() + } catch (e: TokenMismatchException) { + Log.d(javaClass.simpleName, "Mismatcho SSO server request error $e") + throw e + } catch (e: NextcloudHttpRequestFailedException) { + Log.d(javaClass.simpleName, "SSO server HTTP request failed ${e.statusCode}") + throw e + } catch (e: Exception) { + Log.d(javaClass.simpleName, "SSO server request error $e") + } + return VersatileProjectSyncClient.ResponseData(result.toString(), "", 0) + } + + @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + private fun imageRequestServerWithSSO( + nextcloudAPI: NextcloudAPI, + target: String, + method: String, + params: Collection? + ): VersatileProjectSyncClient.ResponseData { + var strBase64 = "" + val nextcloudRequest: NextcloudRequest = if (params == null) { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .build() + } else { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .setParameter(params) + .build() + } + try { + val response = nextcloudAPI.performNetworkRequestV2(nextcloudRequest) + val inputStream = response.body + val selectedImage = BitmapFactory.decodeStream(inputStream) + val stream = ByteArrayOutputStream() + selectedImage.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + strBase64 = Base64.encodeToString(byteArray, 0) + inputStream.close() + } catch (e: TokenMismatchException) { + Log.d(javaClass.simpleName, "Mismatcho SSO server request error $e") + throw e + } catch (e: NextcloudHttpRequestFailedException) { + Log.d(javaClass.simpleName, "SSO server HTTP request failed ${e.statusCode}") + throw e + } catch (e: Exception) { + Log.d(javaClass.simpleName, "SSO server request error $e") + } + return VersatileProjectSyncClient.ResponseData(strBase64, "", 0) + } + + @Throws(IOException::class, NextcloudHttpRequestFailedException::class) + private fun requestServer( + target: String, + method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean + ): VersatileProjectSyncClient.ResponseData { + val result = StringBuilder() + val targetURL = url + target.replace("^/".toRegex(), "") + Log.d(javaClass.simpleName, "method and target URL: $method $targetURL") + val httpCon = SupportUtil.getHttpURLConnection(targetURL) + httpCon.requestMethod = method + if (needLogin) { + httpCon.setRequestProperty( + "Authorization", + "Basic " + Base64.encodeToString("$username:$password".toByteArray(), Base64.NO_WRAP) + ) + } + httpCon.setRequestProperty("Connection", "Close") + httpCon.setRequestProperty("User-Agent", "cowspent-android/" + SupportUtil.getAppVersionName(context)) + if (lastETag != null && METHOD_GET == method) { + httpCon.setRequestProperty("If-None-Match", lastETag) + } + if (isOCSRequest) { + httpCon.setRequestProperty("OCS-APIRequest", "true") + httpCon.setRequestProperty("Accept", "application/json") + } + httpCon.connectTimeout = 10 * 1000 // 10 seconds + var paramData: ByteArray? = null + if (params != null) { + paramData = params.toString().toByteArray() + Log.d(javaClass.simpleName, "Params: $params") + httpCon.setFixedLengthStreamingMode(paramData.size) + httpCon.setRequestProperty("Content-Type", application_json) + httpCon.doOutput = true + val os = httpCon.outputStream + os.write(paramData) + os.flush() + os.close() + } + val responseCode = httpCon.responseCode + Log.d(javaClass.simpleName, "HTTP response code: $responseCode") + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + throw ServerResponse.NotModifiedException() + } + if (responseCode >= 400) { + throw NextcloudHttpRequestFailedException(context, responseCode, IOException("")) + } + Log.i(TAG, "METHOD : $method") + val rd = BufferedReader(InputStreamReader(httpCon.inputStream)) + var line: String? + while (rd.readLine().also { line = it } != null) { + result.append(line) + } + val etag = httpCon.getHeaderField("ETag") + val lastModified = httpCon.getHeaderFieldDate("Last-Modified", 0) / 1000 + Log.i( + javaClass.simpleName, + "Result length: " + result.length + (if (paramData == null) "" else "; Request length: " + paramData.size) + ) + Log.d(javaClass.simpleName, "ETag: $etag; Last-Modified: $lastModified (${httpCon.getHeaderField("Last-Modified")})") + return VersatileProjectSyncClient.ResponseData(result.toString(), "", 0) + } + + @Throws(IOException::class, NextcloudHttpRequestFailedException::class) + private fun imageRequestServer( + target: String, + method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean + ): VersatileProjectSyncClient.ResponseData { + var strBase64: String + val targetURL = url + target.replace("^/".toRegex(), "") + val httpCon = SupportUtil.getHttpURLConnection( targetURL) + httpCon.requestMethod = method + if (needLogin) { + httpCon.setRequestProperty( + "Authorization", + "Basic " + Base64.encodeToString("$username:$password".toByteArray(), Base64.NO_WRAP) + ) + } + httpCon.setRequestProperty("Connection", "Close") + httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context)) + if (lastETag != null && METHOD_GET == method) { + httpCon.setRequestProperty("If-None-Match", lastETag) + } + if (isOCSRequest) { + httpCon.setRequestProperty("OCS-APIRequest", "true") + } + httpCon.connectTimeout = 10 * 1000 // 10 seconds + Log.d(javaClass.simpleName, "$method $targetURL") + var paramData: ByteArray? + if (params != null) { + paramData = params.toString().toByteArray() + Log.d(javaClass.simpleName, "Params: $params") + httpCon.setFixedLengthStreamingMode(paramData.size) + httpCon.setRequestProperty("Content-Type", application_json) + httpCon.doOutput = true + val os = httpCon.outputStream + os.write(paramData) + os.flush() + os.close() + } + val responseCode = httpCon.responseCode + Log.d(javaClass.simpleName, "HTTP response code: $responseCode") + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + throw ServerResponse.NotModifiedException() + } + if (responseCode >= 400) { + throw NextcloudHttpRequestFailedException(context, responseCode, IOException("")) + } + Log.i(TAG, "METHOD : $method") + val selectedImage = BitmapFactory.decodeStream(httpCon.inputStream) + val stream = ByteArrayOutputStream() + selectedImage.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + strBase64 = Base64.encodeToString(byteArray, 0) + return VersatileProjectSyncClient.ResponseData(strBase64, "", 0) + } + + companion object { + private val TAG = NextcloudClient::class.java.simpleName + const val METHOD_GET = "GET" + const val METHOD_POST = "POST" + private const val application_json = "application/json" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt new file mode 100644 index 0000000..c8416ee --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt @@ -0,0 +1,776 @@ +package net.helcel.cowspent.util + +import android.util.Log +import net.helcel.cowspent.model.DBAccountProject +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBBillOwer +import net.helcel.cowspent.model.DBCategory +import net.helcel.cowspent.model.DBCurrency +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBPaymentMode +import net.helcel.cowspent.model.DBProject +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.xml.sax.SAXException +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.parsers.ParserConfigurationException + + +/** + * Provides entity classes for handling server responses + */ +@Suppress("unused") +open class ServerResponse( + private val response: VersatileProjectSyncClient.ResponseData, + protected val isOcsResponse: Boolean +) { + private val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) + + class NotModifiedException : IOException() + + protected val content: String + get() = response.content + + val lastModified: Long + get() = response.lastModified + + @Throws(JSONException::class) + fun getResponseObjectData(): JSONObject { + val rawData = JSONObject(content) + if (!isOcsResponse) { + return rawData + } + val data = rawData.getJSONObject("ocs") + return data.getJSONObject("data") + } + + @Throws(JSONException::class) + fun getResponseArrayData(): JSONArray { + if (!isOcsResponse) { + return JSONArray(content) + } + val rawData = JSONObject(content) + val data = rawData.getJSONObject("ocs") + return data.getJSONArray("data") + } + + @Throws(JSONException::class) + fun getResponseStringData(): String { + if (!isOcsResponse) { + return content + } + val rawData = JSONObject(content) + val data = rawData.getJSONObject("ocs") + return data.getString("data") + } + + class ProjectResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : + ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val email: String + get() = getEmailFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val name: String + get() = getNameFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val deletionDisabled: Boolean + get() = getDeletionDisabledFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val archivedTs: Long? + get() = getArchivedTsFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val myAccessLevel: Int + get() = getMyAccessLevelFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val currencyName: String + get() = getCurrencyNameFromJSON(getResponseObjectData()) + + @Throws(JSONException::class) + fun getMembers(projId: Long): List { + return getMembersFromJSON(getResponseObjectData(), projId) + } + + @Throws(JSONException::class) + fun getCategories(projId: Long): List { + return getCategoriesFromJSON(getResponseObjectData(), projId) + } + + @Throws(JSONException::class) + fun getPaymentModes(projId: Long): List { + return getPaymentModesFromJSON(getResponseObjectData(), projId) + } + + @Throws(JSONException::class) + fun getCurrencies(projId: Long): List { + return getCurrenciesFromJSON(getResponseObjectData(), projId) + } + } + + class CreateRemoteMemberResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean, + private val isJsonMember: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remoteMemberId: Long + get() = if (isJsonMember) + getRemoteMemberIdFromJSON(getResponseObjectData()) + else + getResponseStringData().toLong() + } + + class CreateRemoteCurrencyResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class EditRemoteCurrencyResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class DeleteRemoteCurrencyResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class EditRemoteProjectResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class EditRemoteMemberResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getRemoteId(projectId: Long): Long { + return getMemberFromJSON(getResponseObjectData(), projectId).remoteId + } + } + + class EditRemoteBillResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class CreateRemoteBillResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class DeleteRemoteBillResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class DeleteRemoteProjectResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class CreateRemoteProjectResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class BillsResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : + ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getBillsCospend(projId: Long, memberRemoteIdToId: Map): List { + return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId) + } + + @Throws(JSONException::class) + fun getBillsIHM(projId: Long, memberRemoteIdToId: Map): List { + return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId) + } + + + @get:Throws(JSONException::class) + val allBillIds: List + get() = getAllBillIdsFromJSON(getResponseObjectData()) + + @get:Throws(JSONException::class) + val syncTimestamp: Long + get() = getSyncTimestampFromJSON(getResponseObjectData()) + } + + class MembersResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : + ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getMembers(projId: Long): List { + return getMembersFromJSONArray(getResponseArrayData(), projId) + } + } + + class AccountProjectsResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getAccountProjects(ncUrl: String): List { + return getAccountProjectsFromJSONArray(getResponseArrayData(), ncUrl) + } + } + + class CapabilitiesResponse(response: VersatileProjectSyncClient.ResponseData) : + ServerResponse(response, true) { + + @get:Throws(IOException::class, JSONException::class) + val color: String? + get() = getColorFromJsonContent(JSONObject(content)) + + @get:Throws(JSONException::class) + val cospendVersion: String? + get() = getCospendVersionFromCapabilitiesContent(JSONObject(content)) + } + + class AvatarResponse(response: VersatileProjectSyncClient.ResponseData) : + ServerResponse(response, false) { + + @get:Throws(IOException::class) + val avatarString: String + get() = content + } + + @Throws(JSONException::class) + protected fun getPublicTokenFromJSON(json: JSONObject): String? { + if (json.has("code") && json.has("sharetoken")) { + val done = json.getInt("code") + val publicToken = json.getString("sharetoken") + if (done == 1) { + return publicToken + } + } + return null + } + + @Throws(JSONException::class) + protected fun getNameFromJSON(json: JSONObject): String { + return if (json.has("name") && !json.isNull("name")) { + json.getString("name") + } else "" + } + + @Throws(JSONException::class) + protected fun getDeletionDisabledFromJSON(json: JSONObject): Boolean { + return if (json.has("deletiondisabled")) { + json.getBoolean("deletiondisabled") + } else false + } + + @Throws(JSONException::class) + protected fun getArchivedTsFromJSON(json: JSONObject): Long? { + return if (json.has("archived_ts") && !json.isNull("archived_ts")) { + val ts = json.optLong("archived_ts", 0) + if (ts > 0) ts else null + } else null + } + + @Throws(JSONException::class) + protected fun getMyAccessLevelFromJSON(json: JSONObject): Int { + return if (json.has("myaccesslevel")) { + json.getInt("myaccesslevel") + } else DBProject.ACCESS_LEVEL_UNKNOWN + } + + @Throws(JSONException::class) + protected fun getCurrencyNameFromJSON(json: JSONObject): String { + return if (json.has("currencyname") && !json.isNull("currencyname")) { + json.getString("currencyname") + } else "" + } + + @Throws(JSONException::class) + protected fun getEmailFromJSON(json: JSONObject): String { + return if (json.has("contact_email") && !json.isNull("contact_email")) { + json.getString("contact_email") + } else "" + } + + @Throws(JSONException::class) + protected fun getMembersFromJSONArray(jsonMs: JSONArray, projId: Long): List { + val members: MutableList = ArrayList() + for (i in 0 until jsonMs.length()) { + val jsonM = jsonMs.getJSONObject(i) + members.add(getMemberFromJSON(jsonM, projId)) + } + return members + } + + @Throws(JSONException::class) + protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List { + val categories: MutableList = ArrayList() + if (json.has("categories") && json.get("categories") is JSONObject) { + val jsonCats = json.getJSONObject("categories") + val keys = jsonCats.keys() + while (keys.hasNext()) { + val key = keys.next() + if (jsonCats.get(key) is JSONObject) { + categories.add(getCategoryFromJSON(jsonCats.getJSONObject(key), key, projId)) + } + } + } + return categories + } + + @Throws(JSONException::class) + protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory { + val remoteId = remoteIdStr.toLong() + var name = "" + var color = "" + var icon = "" + if (json.has("color") && !json.isNull("color")) { + color = json.getString("color") + } + if (json.has("icon") && !json.isNull("icon")) { + icon = json.getString("icon") + } + if (json.has("name") && !json.isNull("name")) { + name = json.getString("name") + } + return DBCategory(0, remoteId, projId, name, icon, color) + } + + @Throws(JSONException::class) + protected fun getPaymentModesFromJSON(json: JSONObject, projId: Long): List { + val paymentModes: MutableList = ArrayList() + if (json.has("paymentmodes") && json.get("paymentmodes") is JSONObject) { + val jsonPms = json.getJSONObject("paymentmodes") + val keys = jsonPms.keys() + while (keys.hasNext()) { + val key = keys.next() + if (jsonPms.get(key) is JSONObject) { + paymentModes.add(getPaymentModeFromJSON(jsonPms.getJSONObject(key), key, projId)) + } + } + } + return paymentModes + } + + @Throws(JSONException::class) + protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode { + val remoteId = remoteIdStr.toLong() + var name = "" + var color = "" + var icon = "" + if (json.has("color") && !json.isNull("color")) { + color = json.getString("color") + } + if (json.has("icon") && !json.isNull("icon")) { + icon = json.getString("icon") + } + if (json.has("name") && !json.isNull("name")) { + name = json.getString("name") + } + return DBPaymentMode(0, remoteId, projId, name, icon, color) + } + + @Throws(JSONException::class) + protected fun getCurrenciesFromJSON(json: JSONObject, projId: Long): List { + val currencies: MutableList = ArrayList() + if (json.has("currencies") && json.get("currencies") is JSONArray) { + val jsonCurs = json.getJSONArray("currencies") + for (i in 0 until jsonCurs.length()) { + if (jsonCurs.get(i) is JSONObject) { + currencies.add(getCurrencyFromJSON(jsonCurs.getJSONObject(i), projId)) + } + } + } + return currencies + } + + @Throws(JSONException::class) + protected fun getCurrencyFromJSON(json: JSONObject, projId: Long): DBCurrency { + var remoteId: Long = 0 + var name = "" + var exchangeRate = 1.0 + if (json.has("exchange_rate") && !json.isNull("exchange_rate")) { + exchangeRate = json.getDouble("exchange_rate") + } + if (json.has("id") && !json.isNull("id")) { + remoteId = json.getLong("id") + } + if (json.has("name") && !json.isNull("name")) { + name = json.getString("name") + } + return DBCurrency(0, remoteId, projId, name, exchangeRate, DBBill.STATE_OK) + } + + @Throws(JSONException::class) + protected fun getMembersFromJSON(json: JSONObject, projId: Long): List { + val members: MutableList = ArrayList() + if (json.has("members")) { + val jsonMs = json.getJSONArray("members") + for (i in 0 until jsonMs.length()) { + val jsonM = jsonMs.getJSONObject(i) + members.add(getMemberFromJSON(jsonM, projId)) + } + } + return members + } + + @Throws(JSONException::class) + protected fun getMemberFromJSON(json: JSONObject, projId: Long): DBMember { + var activated = true + var weight = 1.0 + var remoteId: Long = 0 + var name = "" + var r: Int? = null + var g: Int? = null + var b: Int? = null + var ncUserId: String? = null + if (!json.isNull("id")) { + remoteId = json.getLong("id") + } + if (!json.isNull("weight")) { + weight = json.getDouble("weight") + } + if (!json.isNull("activated")) { + activated = json.getBoolean("activated") + } + if (!json.isNull("name")) { + name = json.getString("name") + } + if (json.has("color") && !json.isNull("color")) { + val obj = json.get("color") + if (obj is String) { + val color = json.getString("color").replace("#", "") + if (color.length == 6) { + r = color.substring(0, 2).toInt(16) + g = color.substring(2, 4).toInt(16) + b = color.substring(4, 6).toInt(16) + } + } else if (obj is JSONObject) { + val color = json.getJSONObject("color") + if (color.has("r") && !color.isNull("r")) { + r = color.getInt("r") + } + if (color.has("g") && !color.isNull("g")) { + g = color.getInt("g") + } + if (color.has("b") && !color.isNull("b")) { + b = color.getInt("b") + } + } + } + if (json.has("userid") && !json.isNull("userid")) { + ncUserId = json.getString("userid") + } + return DBMember( + 0, remoteId, projId, name, activated, weight, DBBill.STATE_OK, + r, g, b, ncUserId, null + ) + } + + @Throws(JSONException::class) + protected fun getAllBillIdsFromJSON(json: JSONObject): List { + val billIds: MutableList = ArrayList() + if (json.has("allBillIds") && !json.isNull("allBillIds")) { + val jsonBillIds = json.getJSONArray("allBillIds") + for (i in 0 until jsonBillIds.length()) { + billIds.add(jsonBillIds.getLong(i)) + } + } + return billIds + } + + @Throws(JSONException::class) + protected fun getSyncTimestampFromJSON(json: JSONObject): Long { + var ts = 0L + if (json.has("timestamp") && !json.isNull("timestamp")) { + ts = json.getLong("timestamp") + } + return ts + } + + @Throws(JSONException::class) + protected fun getBillsFromJSONArray( + json: JSONArray, + projId: Long, + memberRemoteIdToId: Map + ): List { + val bills: MutableList = ArrayList() + for (i in 0 until json.length()) { + val jsonBill = json.getJSONObject(i) + bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId)) + } + return bills + } + + @Throws(JSONException::class) + protected fun getBillsFromJSONObject( + json: JSONObject, + projId: Long, + memberRemoteIdToId: Map + ): List { + val bills: List + if (json.has("bills") && !json.isNull("bills")) { + val jsonBills = json.getJSONArray("bills") + bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId) + } else { + bills = ArrayList() + } + return bills + } + + @Throws(JSONException::class) + protected fun getBillFromJSON( + json: JSONObject, + projId: Long, + memberRemoteIdToId: Map + ): DBBill { + var remoteId: Long = 0 + var payerRemoteId: Long + var payerId: Long = 0 + var amount = 0.0 + var dateStr: String + var date: Date + var timestamp: Long = 0 + var what = "" + var comment = "" + var repeat = DBBill.NON_REPEATED + var paymentMode = DBBill.PAYMODE_NONE + var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE + var categoryId = DBBill.CATEGORY_NONE + if (!json.isNull("id")) { + remoteId = json.getLong("id") + } + if (!json.isNull("payer_id")) { + payerRemoteId = json.getLong("payer_id") + payerId = memberRemoteIdToId[payerRemoteId] ?: 0 + } else if (!json.isNull("payer")) { + payerRemoteId = json.getLong("payer") + payerId = memberRemoteIdToId[payerRemoteId] ?: 0 + } + if (!json.isNull("amount")) { + amount = json.getDouble("amount") + } + // get timestamp in priority + if (!json.isNull("timestamp")) { + timestamp = json.getLong("timestamp") + } else if (!json.isNull("date")) { + dateStr = json.getString("date") + try { + date = sdf.parse(dateStr)!! + timestamp = date.time / 1000 + } catch (_: Exception) { + timestamp = 0 + } + } + if (!json.isNull("what")) { + what = json.getString("what") + } else if (!json.isNull("label")) { + what = json.getString("label") + } + if (!json.isNull("comment")) { + comment = json.getString("comment") + } + if (json.has("repeat") && !json.isNull("repeat")) { + repeat = json.getString("repeat") + } + if (json.has("paymentmode") && !json.isNull("paymentmode")) { + paymentMode = json.getString("paymentmode") + } + if (json.has("categoryid") && !json.isNull("categoryid")) { + categoryId = json.getInt("categoryid") + Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId") + } + if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) { + paymentModeRemoteId = json.getInt("paymentmodeid") + } + // old MB, new Cospend is ok as Cospend provides the old pm ID + // new MB, old Cospend => set payment mode ID from old one + if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) { + Log.d("PaymentMode", "old: $paymentMode and new: 0") + paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE + } + val bill = DBBill( + 0, remoteId, projId, payerId, amount, timestamp, what, + DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeRemoteId + ) + bill.billOwers = getBillOwersFromJson(json, memberRemoteIdToId) + return bill + } + + @Throws(JSONException::class) + protected fun getBillOwersFromJson( + json: JSONObject, + memberRemoteIdToId: Map + ): List { + val billOwers: MutableList = ArrayList() + if (json.has("owers")) { + val jsonOs = json.getJSONArray("owers") + for (i in 0 until jsonOs.length()) { + val obj = jsonOs.get(i) + val memberRemoteId = if (obj is JSONObject) { + obj.getLong("id") + } else { + jsonOs.getLong(i) + } + val memberLocalId = memberRemoteIdToId[memberRemoteId] ?: 0 + billOwers.add(DBBillOwer(0, 0, memberLocalId)) + } + } + return billOwers + } + + @Throws(JSONException::class) + protected fun getAccountProjectsFromJSONArray(jsonMs: JSONArray, ncUrl: String): List { + val accountProjects: MutableList = ArrayList() + for (i in 0 until jsonMs.length()) { + val jsonAP = jsonMs.getJSONObject(i) + accountProjects.add(getAccountProjectFromJSON(jsonAP, ncUrl)) + } + return accountProjects + } + + @Throws(JSONException::class) + protected fun getAccountProjectFromJSON(json: JSONObject, accountNcUrl: String): DBAccountProject { + var remoteId = "" + var name = "" + var ncUrl = "" + if (!json.isNull("name")) { + name = json.getString("name") + } + if (!json.isNull("id")) { + remoteId = json.getString("id") + } + if (!json.isNull("ncurl")) { + ncUrl = json.getString("ncUrl") + } + val archivedTs: Long? = getArchivedTsFromJSON(json) + if (ncUrl.isEmpty()) { + ncUrl = accountNcUrl + } + return DBAccountProject(0, remoteId, null, name, ncUrl, archivedTs) + } + + @Throws(IOException::class) + protected fun getColorFromContent(content: String): String? { + var result: String? = null + try { + val dbf = DocumentBuilderFactory.newInstance() + val db = dbf.newDocumentBuilder() + val stream: InputStream = ByteArrayInputStream(content.toByteArray()) + val doc = db.parse(stream) + doc.documentElement.normalize() + // Locate the Tag Name + val nodeList = doc.getElementsByTagName("color") + if (nodeList.length > 0) { + result = nodeList.item(0).textContent + Log.i(TAG, "I GOT THE COLOR from server: $result") + } + } catch (_: ParserConfigurationException) { + } catch (_: SAXException) { + } + return result + } + + protected fun getColorFromJsonContent(json: JSONObject): String? { + return try { + val ocs = json.getJSONObject("ocs") + val data = ocs.getJSONObject("data") + val capabilities = data.getJSONObject("capabilities") + val theming = capabilities.getJSONObject("theming") + val color = theming.getString("color") + Log.i(TAG, "I GOT THE COLOR from server's JSON response: $color") + color + } catch (e: JSONException) { + Log.e(TAG, "Failed to get the color from OCS capabilities response $e") + null + } + } + + protected fun getCospendVersionFromCapabilitiesContent(json: JSONObject): String? { + return try { + val ocs = json.getJSONObject("ocs") + val data = ocs.getJSONObject("data") + val capabilities = data.getJSONObject("capabilities") + val cospend = capabilities.getJSONObject("cospend") + val version = cospend.getString("version") + Log.i(TAG, "I GOT THE Cospend version: $version") + version + } catch (e: JSONException) { + Log.i(TAG, "Failed to get the Cospend version$e") + null + } + } + + @Throws(JSONException::class) + protected fun getRemoteMemberIdFromJSON(json: JSONObject): Long { + return json.getLong("id") + } + + companion object { + private val TAG = ServerResponse::class.java.simpleName + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt b/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt new file mode 100644 index 0000000..00766a4 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt @@ -0,0 +1,338 @@ +package net.helcel.cowspent.util + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import net.helcel.cowspent.model.* +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URL +import java.security.KeyManagementException +import java.security.NoSuchAlgorithmException +import java.text.NumberFormat +import java.util.Locale +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.round + +object SupportUtil { + + @JvmField + val normalNumberFormat: NumberFormat = NumberFormat.getInstance() + + @JvmField + val dotNumberFormat: NumberFormat = NumberFormat.getNumberInstance(Locale.UK) + + @JvmField + val commaNumberFormat: NumberFormat = NumberFormat.getNumberInstance(Locale.GERMANY) + + init { + normalNumberFormat.maximumFractionDigits = 2 + normalNumberFormat + dotNumberFormat.maximumFractionDigits = Int.MAX_VALUE + dotNumberFormat.isGroupingUsed = false + commaNumberFormat.maximumFractionDigits = Int.MAX_VALUE + commaNumberFormat.isGroupingUsed = false + } + + @JvmStatic + @Throws(MalformedURLException::class, IOException::class) + fun getHttpURLConnection(strUrl: String): HttpURLConnection { + val url = URL(strUrl) + val httpCon = url.openConnection() as HttpURLConnection + if (url.protocol == "https") { + val httpsCon = httpCon as HttpsURLConnection + try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, null, null) + httpsCon.sslSocketFactory = sslContext.socketFactory + } catch (e: NoSuchAlgorithmException) { + Log.e(SupportUtil::class.java.simpleName, "Exception", e) + } catch (e: KeyManagementException) { + Log.e(SupportUtil::class.java.simpleName, "Exception", e) + } + } + return httpCon + } + + @JvmStatic + fun isDouble(s: String?): Boolean { + if (s == null) return false + return try { + s.toDouble() + true + } catch (_: NumberFormatException) { + false + } + } + + @JvmStatic + fun isValidEmail(target: CharSequence?): Boolean { + return if (target == null) false else android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches() + } + + @JvmStatic + fun getStatsOfProject( + projId: Long, db: CowspentSQLiteOpenHelper, + membersNbBills: MutableMap, + membersBalance: MutableMap, + membersPaid: MutableMap, + membersSpent: MutableMap, + catId: Int, paymentModeId: Int, + dateMin: String?, dateMax: String? + ): Int { + return getStats( + db.getMembersOfProject(projId, null), + db.getBillsOfProject(projId), + membersNbBills, membersBalance, membersPaid, membersSpent, + catId, paymentModeId, dateMin, dateMax + ) + } + + @JvmStatic + fun getStats( + dbMembers: List, + dbBills: List, + membersNbBills: MutableMap, + membersBalance: MutableMap, + membersPaid: MutableMap, + membersSpent: MutableMap, + catId: Int, paymentModeId: Int, + dateMin: String?, dateMax: String? + ): Int { + val nbBillsTotal = 0 + val membersWeight: MutableMap = HashMap() + + // init + for (m in dbMembers) { + membersNbBills[m.id] = 0 + membersBalance[m.id] = 0.0 + membersPaid[m.id] = 0.0 + membersSpent[m.id] = 0.0 + membersWeight[m.id] = m.weight + } + + for (b in dbBills) { + // don't take deleted bills and respect category filter + if (b.state != DBBill.STATE_DELETED && + ((catId == -1000 || catId == -100 || b.categoryRemoteId == catId) && + (catId != -100 || b.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT) && + (paymentModeId == -1000 || b.paymentModeRemoteId == paymentModeId)) && + (dateMin == null || b.date >= dateMin) && + (dateMax == null || b.date <= dateMax) + ) { + val nb = membersNbBills[b.payerId] ?: 0 + membersNbBills[b.payerId] = nb + 1 + val amount = b.amount + val balPayer = membersBalance[b.payerId] ?: 0.0 + membersBalance[b.payerId] = balPayer + amount + val paid = membersPaid[b.payerId] ?: 0.0 + membersPaid[b.payerId] = paid + amount + + var nbOwerShares = 0.0 + for (bo in b.billOwers) { + nbOwerShares += membersWeight[bo.memberId] ?: 0.0 + } + for (bo in b.billOwers) { + val owerWeight = membersWeight[bo.memberId] ?: 0.0 + val spent = if (nbOwerShares > 0) amount / nbOwerShares * owerWeight else 0.0 + val balOwer = membersBalance[bo.memberId] ?: 0.0 + membersBalance[bo.memberId] = balOwer - spent + val spentOwer = membersSpent[bo.memberId] ?: 0.0 + membersSpent[bo.memberId] = spentOwer + spent + } + } + } + return nbBillsTotal + } + + @JvmStatic + fun round2(n: Double): Double { + var r = round(abs(n) * 100.0) / 100.0 + if (n < 0.0) r = -r + return r + } + + const val SETTLE_OPTIMAL: Long = 0 + + @JvmStatic + fun settleBills( + members: List, membersBalance: Map, + centerOnMemberId: Long + ): List { + return if (centerOnMemberId == SETTLE_OPTIMAL) { + settleBillsOptimal(members, membersBalance) + } else { + val results: MutableList = ArrayList() + for (mid in membersBalance.keys) { + if (mid != centerOnMemberId) { + val balance = membersBalance[mid] ?: 0.0 + if (balance > 0.0) { + results.add(Transaction(centerOnMemberId, mid, balance)) + } else if (balance < 0.0) { + results.add(Transaction(mid, centerOnMemberId, -balance)) + } + } + } + results + } + } + + @JvmStatic + fun settleBillsOptimal(members: List, membersBalance: Map): List { + val crediters: MutableList = ArrayList() + val debiters: MutableList = ArrayList() + + // Create lists of credits and debts + for (m in members) { + val memberId = m.id + val balance = membersBalance[memberId] ?: 0.0 + + if (round2(balance) > 0.0) { + crediters.add(CreditDebt(memberId, balance)) + } else if (round2(balance) < 0.0) { + debiters.add(CreditDebt(memberId, balance)) + } + } + + return reduceBalance(crediters, debiters, null) + } + + @JvmStatic + fun reduceBalance( + crediters: MutableList, + debiters: MutableList, + resultsParam: MutableList? + ): List { + var results = resultsParam + if (debiters.isEmpty() || crediters.isEmpty()) { + return results ?: emptyList() + } + + if (results == null) { + results = ArrayList() + } + + crediters.sortWith { cd2, cd1 -> + if (cd1.balance == cd2.balance) { + 0 + } else { + if (cd1.balance < cd2.balance) 1 else -1 + } + } + + for (c in crediters) { + Log.e(SupportUtil::class.java.simpleName, "* " + c.memberId + " : " + c.balance) + } + + debiters.sortWith { cd2, cd1 -> + if (cd1.balance == cd2.balance) { + 0 + } else { + if (cd1.balance > cd2.balance) 1 else -1 + } + } + + val deb = debiters.removeAt(debiters.size - 1) + val debiter = deb.memberId + val debiterBalance = deb.balance + + val cred = crediters.removeAt(crediters.size - 1) + val crediter = cred.memberId + val crediterBalance = cred.balance + + val amount: Double = if (abs(debiterBalance) > abs(crediterBalance)) { + abs(crediterBalance) + } else { + abs(debiterBalance) + } + + results.add(Transaction(debiter, crediter, amount)) + + val newDebiterBalance = debiterBalance + amount + if (newDebiterBalance < 0.0) { + debiters.add(CreditDebt(debiter, newDebiterBalance)) + debiters.sortWith { cd2, cd1 -> + if (cd1.balance == cd2.balance) { + 0 + } else { + if (cd1.balance > cd2.balance) 1 else -1 + } + } + } + + val newCrediterBalance = crediterBalance - amount + if (newCrediterBalance > 0.0) { + crediters.add(CreditDebt(crediter, newCrediterBalance)) + crediters.sortWith { cd2, cd1 -> + if (cd1.balance == cd2.balance) { + 0 + } else { + if (cd1.balance < cd2.balance) 1 else -1 + } + } + } + + return reduceBalance(crediters, debiters, results) + } + + @JvmStatic + fun getVersionName(context: Context): String { + var versionName = "0.0.0" + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + versionName = pInfo.versionName ?: "0.0.0" + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + return versionName + } + + @JvmStatic + fun getJsonObject(text: String?): JSONObject? { + if (text == null) return null + return try { + JSONObject(text) + } catch (_: JSONException) { + null + } + } + + @JvmStatic + fun compareVersions(version1: String, version2: String): Int { + val levels1 = version1.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val levels2 = version2.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val length = max(levels1.size, levels2.size) + for (i in 0 until length) { + val v1 = if (i < levels1.size) levels1[i].toInt() else 0 + val v2 = if (i < levels2.size) levels2[i].toInt() else 0 + val compare = v1.compareTo(v2) + if (compare != 0) { + return compare + } + } + return 0 + } + + @JvmStatic + fun getAppVersionName(context: Context): String { + var versionName = "???" + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + versionName = pInfo.versionName ?: "0.0.0" + } catch (e: PackageManager.NameNotFoundException) { + Log.e(SupportUtil::class.java.simpleName, "Failed to get app version name", e) + e.printStackTrace() + } + Log.d(SupportUtil::class.java.simpleName, "app version name is $versionName") + return versionName + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt new file mode 100644 index 0000000..ac0a4df --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt @@ -0,0 +1,1130 @@ +package net.helcel.cowspent.util + +import android.content.Context +import android.util.Base64 +import android.util.Log +import androidx.annotation.WorkerThread +import com.nextcloud.android.sso.QueryParam +import com.nextcloud.android.sso.aidl.NextcloudRequest +import com.nextcloud.android.sso.api.NextcloudAPI +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException +import com.nextcloud.android.sso.exceptions.TokenMismatchException +import com.nextcloud.android.sso.model.SingleSignOnAccount +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.model.DBCurrency +import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.model.DBProject +import net.helcel.cowspent.model.ProjectType +import org.json.JSONException +import java.io.* +import java.net.HttpURLConnection +import java.net.URLEncoder + +@WorkerThread +class VersatileProjectSyncClient( + private val url: String, + private val username: String, + private val password: String, + private val nextcloudAPI: NextcloudAPI?, + private val ssoAccount: SingleSignOnAccount?, + cospendVersion: String?, + private val context: Context +) { + + /** + * This entity class is used to return relevant data of the HTTP response. + */ + class ResponseData(val content: String, val eTag: String?, val lastModified: Long) + + private val cospendVersionGT161: Boolean = if (cospendVersion == null) { + Log.i(TAG, "GT161 is FALSE") + false + } else { + val gt = SupportUtil.compareVersions(cospendVersion, "1.6.1") >= 0 + Log.i(TAG, "GT161: $gt") + gt + } + + fun canAccessProjectWithNCLogin(project: DBProject): Boolean { + return (project.password == "" + && url.replace("/+$".toRegex(), "") != "" + && project.serverUrl!! + .replace("/index.php/apps/cospend", "") == url.replace("/+$".toRegex(), "") + ) + } + + fun canAccessProjectWithSSO(project: DBProject): Boolean { + return (project.password == "" + && ssoAccount != null + && project.serverUrl!!.replace("/index.php/apps/cospend", "") == ssoAccount.url + ) + } + + @Throws(UnsupportedEncodingException::class) + private fun getEncodedPassword(password: String): String { + return URLEncoder.encode(password, "utf-8").replace("+", "%20") + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getProject(project: DBProject, lastModified: Long, lastETag: String?): ServerResponse.ProjectResponse { + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for getProjectInfo") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + Log.i(TAG, "using new API for getProjectInfo") + ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + Log.i(TAG, "using public API, target is: ${target}for getProjectInfo") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.ProjectResponse( + requestServer( + target, METHOD_GET, null, null, lastETag, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemoteProject( + project: DBProject, newName: String?, newEmail: String?, newPassword: String?, + newMainCurrencyName: String? + ): ServerResponse.EditRemoteProjectResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + if (newName != null) { + paramKeys.add("name") + paramValues.add(newName) + } + if (newEmail != null) { + paramKeys.add("contact_email") + paramValues.add(newEmail) + } + if (newPassword != null) { + paramKeys.add("password") + paramValues.add(newPassword) + } + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (newMainCurrencyName != null) { + paramKeys.add("currencyname") + paramValues.add(newMainCurrencyName) + } + if (cospendVersionGT161) { + paramKeys.clear() + paramValues.clear() + paramKeys.add("id") + paramValues.add(project.remoteId) + if (newName != null) { + paramKeys.add("name") + paramValues.add(newName) + } + if (newPassword != null) { + paramKeys.add("password") + paramValues.add(newPassword) + } + if (newMainCurrencyName != null) { + paramKeys.add("currencyName") + paramValues.add(newMainCurrencyName) + } + } + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteProject") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + Log.i(TAG, "using new API for editRemoteProject") + ServerResponse.EditRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + ServerResponse.EditRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + Log.i(TAG, "using public API, target is: ${target}for editRemoteProject") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.EditRemoteProjectResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemoteMember(project: DBProject, member: DBMember): ServerResponse.EditRemoteMemberResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(member.name) + paramKeys.add("weight") + paramValues.add(member.weight.toString()) + paramKeys.add("activated") + paramValues.add(if (member.isActivated) "true" else "false") + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + val r = member.r + val g = member.g + val b = member.b + if (r != null && g != null && b != null) { + val hexColor = "#" + Integer.toHexString(r) + Integer.toHexString(g) + Integer.toHexString(b) + paramKeys.add("color") + paramValues.add(hexColor) + } + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/members/" + member.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteMember") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members/" + member.remoteId + Log.i(TAG, "using new API for editRemoteMember") + ServerResponse.EditRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId + ServerResponse.EditRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId + Log.i(TAG, "using public API, target is: ${target}for editRemoteMember") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members/" + member.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.EditRemoteMemberResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map): ServerResponse.EditRemoteBillResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("date") + paramValues.add(bill.date) + if (project.type == ProjectType.IHATEMONEY) { + paramKeys.add("label") + } else { + paramKeys.add("what") + } + paramValues.add(bill.what) + paramKeys.add("payer") + paramValues.add(memberIdToRemoteId[bill.payerId].toString()) + paramKeys.add("amount") + paramValues.add(SupportUtil.dotNumberFormat.format(bill.amount)) + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + paramKeys.add("timestamp") + paramValues.add(bill.timestamp.toString()) + paramKeys.add("comment") + paramValues.add(bill.comment ?: "") + paramKeys.add("repeat") + paramValues.add(bill.repeat ?: "") + + if (cospendVersionGT161) { + paramKeys.add("payedFor") + paramKeys.add("paymentMode") + paramKeys.add("categoryId") + paramKeys.add("paymentModeId") + } else { + paramKeys.add("payed_for") + paramKeys.add("paymentmode") + paramKeys.add("categoryid") + paramKeys.add("paymentmodeid") + } + var payedFor = "" + for (boId in bill.billOwersIds) { + payedFor += memberIdToRemoteId[boId].toString() + "," + } + payedFor = payedFor.replace(",$".toRegex(), "") + paramValues.add(payedFor) + paramValues.add(bill.paymentMode ?: "") + paramValues.add(bill.categoryRemoteId.toString()) + paramValues.add(bill.paymentModeRemoteId.toString()) + + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills/" + bill.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + bill.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteBill") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + bill.remoteId + Log.i(TAG, "using new API for editRemoteBill") + ServerResponse.EditRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills/" + bill.remoteId + ServerResponse.EditRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId + Log.i(TAG, "using public API, target is: ${target}for editRemoteBill") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + bill.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + + val owerKey = if (project.type == ProjectType.IHATEMONEY) "owers" else "payed_for" + for (boId in bill.billOwersIds) { + paramKeys.add(owerKey) + paramValues.add(memberIdToRemoteId[boId].toString()) + } + } + return ServerResponse.EditRemoteBillResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun deleteRemoteProject(project: DBProject): ServerResponse.DeleteRemoteProjectResponse { + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteProject") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + Log.i(TAG, "using new API for deleteRemoteProject") + ServerResponse.DeleteRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + ServerResponse.DeleteRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.DeleteRemoteProjectResponse( + requestServer( + target, METHOD_DELETE, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun deleteRemoteBill(project: DBProject, billRemoteId: Long): ServerResponse.DeleteRemoteBillResponse { + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills/" + billRemoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + billRemoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteBill") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + billRemoteId + Log.i(TAG, "using new API for deleteRemoteProject") + ServerResponse.DeleteRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills/" + billRemoteId + ServerResponse.DeleteRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId + Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + billRemoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.DeleteRemoteBillResponse( + requestServer( + target, METHOD_DELETE, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, NextcloudHttpRequestFailedException::class) + fun createAnonymousRemoteProject(project: DBProject): ServerResponse.CreateRemoteProjectResponse { + val target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects" + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(project.name) + paramKeys.add("contact_email") + paramValues.add(project.email ?: "") + paramKeys.add("password") + paramValues.add(project.password) + paramKeys.add("id") + paramValues.add(project.remoteId) + return ServerResponse.CreateRemoteProjectResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, + null, null, null, null, false + ), false + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createAuthenticatedRemoteProject(project: DBProject): ServerResponse.CreateRemoteProjectResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(project.name) + paramKeys.add("contact_email") + paramValues.add(project.email ?: "") + paramKeys.add("password") + paramValues.add(project.password) + paramKeys.add("id") + paramValues.add(project.remoteId) + + var target: String + var username: String? + var password: String? + var useOcsApiRequest: Boolean + if (ssoAccount != null) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects" + Log.i(TAG, "using new API for createAuthenticatedRemoteProject") + ServerResponse.CreateRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects" + ServerResponse.CreateRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, false), false) + } + } else { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects" + else + project.getRequestBaseUrl(false) + "/api-priv/projects" + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for createAuthenticatedRemoteProject") + } + return ServerResponse.CreateRemoteProjectResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, + null, username, password, null, useOcsApiRequest + ), useOcsApiRequest + ) + } + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map): ServerResponse.CreateRemoteBillResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("date") + paramValues.add(bill.date) + paramKeys.add("what") + paramValues.add(bill.what) + paramKeys.add("payer") + paramValues.add(memberIdToRemoteId[bill.payerId].toString()) + paramKeys.add("amount") + paramValues.add(SupportUtil.dotNumberFormat.format(bill.amount)) + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + paramKeys.add("comment") + paramValues.add(bill.comment ?: "") + paramKeys.add("timestamp") + paramValues.add(bill.timestamp.toString()) + paramKeys.add("repeat") + paramValues.add(bill.repeat ?: "") + if (cospendVersionGT161) { + paramKeys.add("payedFor") + paramKeys.add("paymentMode") + paramKeys.add("categoryId") + paramKeys.add("paymentModeId") + } else { + paramKeys.add("payed_for") + paramKeys.add("paymentmode") + paramKeys.add("categoryid") + paramKeys.add("paymentmodeid") + } + var payedFor = "" + for (boId in bill.billOwersIds) { + payedFor += memberIdToRemoteId[boId].toString() + "," + } + payedFor = payedFor.replace(",$".toRegex(), "") + paramValues.add(payedFor) + paramValues.add(bill.paymentMode ?: "") + paramValues.add(bill.categoryRemoteId.toString()) + paramValues.add(bill.paymentModeRemoteId.toString()) + + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills" + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteBill") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" + Log.i(TAG, "using new API for createRemoteBill") + ServerResponse.CreateRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills" + ServerResponse.CreateRemoteBillResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills" + Log.i(TAG, "using public API, target is: ${target}for createRemoteBill") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + + val owerKey = "payed_for" + for (boId in bill.billOwersIds) { + paramKeys.add(owerKey) + paramValues.add(memberIdToRemoteId[boId].toString()) + } + } + + return ServerResponse.CreateRemoteBillResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createRemoteMember(project: DBProject, member: DBMember): ServerResponse.CreateRemoteMemberResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(member.name) + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + val r = member.r + val g = member.g + val b = member.b + if (r != null && g != null && b != null) { + val hexColor = "#" + Integer.toHexString(r) + Integer.toHexString(g) + Integer.toHexString(b) + paramKeys.add("color") + paramValues.add(hexColor) + } + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/members" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members" + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteMember") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members" + Log.i(TAG, "using new API for createRemoteBill") + ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members" + ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, false), isOcsResponse=false, isJsonMember=false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members" + Log.i(TAG, "using public API, target is: ${target}for createRemoteBill") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.CreateRemoteMemberResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest, + ProjectType.COSPEND == project.type && cospendVersionGT161 + ) + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getBills(project: DBProject): ServerResponse.BillsResponse { + var target: String + var username: String? + var password: String? + var bearerToken: String? + var useOcsApiRequest: Boolean + if (ProjectType.COSPEND == project.type) { + val tsLastSync = project.lastSyncedTimestamp + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills?lastChanged=" + tsLastSync + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for getBills") + } + return ServerResponse.BillsResponse( + requestServer( + target, METHOD_GET, null, null, + null, username, password, null, useOcsApiRequest + ), + useOcsApiRequest + ) + } else if (canAccessProjectWithSSO(project)) { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + if (cospendVersionGT161) { + paramKeys.add("lastChanged") + } else { + paramKeys.add("lastchanged") + } + paramValues.add(tsLastSync.toString()) + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" + Log.i(TAG, "using new API for getBills") + ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills" + ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync + else + project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync + Log.i(TAG, "using public API, target is: ${target}for getBills") + return ServerResponse.BillsResponse( + requestServer( + target, METHOD_GET, null, null, + null, null, null, null, useOcsApiRequest + ), + useOcsApiRequest + ) + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + return ServerResponse.BillsResponse( + requestServer( + target, METHOD_GET, null, null, + null, username, password, bearerToken, false + ), + false + ) + } + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getMembers(project: DBProject): ServerResponse.MembersResponse { + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/members" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members" + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for getMembers, projectId: " + project.remoteId) + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members" + Log.i(TAG, "using new API for getMembers") + ServerResponse.MembersResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members" + ServerResponse.MembersResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members" + Log.i(TAG, "using public API, target is: ${target}for getMembers") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.MembersResponse( + requestServer( + target, METHOD_GET, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(currency.name ?: "") + paramKeys.add("rate") + paramValues.add(currency.exchangeRate.toString()) + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/currency" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency" + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteCurrency") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency" + Log.i(TAG, "using new API for createRemoteCurrency") + ServerResponse.CreateRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/currency" + ServerResponse.CreateRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency" + Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.CreateRemoteCurrencyResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.EditRemoteCurrencyResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(currency.name ?: "") + paramKeys.add("rate") + paramValues.add(currency.exchangeRate.toString()) + + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/currency/" + currency.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currency.remoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteCurrency") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currency.remoteId + Log.i(TAG, "using new API for createRemoteCurrency") + ServerResponse.EditRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/currency/" + currency.remoteId + ServerResponse.EditRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId + Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currency.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.EditRemoteCurrencyResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun deleteRemoteCurrency(project: DBProject, currencyRemoteId: Long): ServerResponse.DeleteRemoteCurrencyResponse { + var target: String + var username: String? = null + var password: String? = null + var bearerToken: String? = null + var useOcsApiRequest = false + if (ProjectType.COSPEND == project.type) { + if (canAccessProjectWithNCLogin(project)) { + username = this.username + password = this.password + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/currency/" + currencyRemoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currencyRemoteId + useOcsApiRequest = cospendVersionGT161 + if (cospendVersionGT161) { + Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteCurrency") + } + } else if (canAccessProjectWithSSO(project)) { + return if (cospendVersionGT161) { + target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currencyRemoteId + Log.i(TAG, "using new API for deleteRemoteCurrency") + ServerResponse.DeleteRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, null, null, true), true) + } else { + target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/currency/" + currencyRemoteId + ServerResponse.DeleteRemoteCurrencyResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, null, null, false), false) + } + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId + Log.i(TAG, "using public API, target is: ${target}for deleteRemoteCurrency") + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currencyRemoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.DeleteRemoteCurrencyResponse( + requestServer( + target, METHOD_DELETE, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + private fun requestServerWithSSO( + nextcloudAPI: NextcloudAPI, target: String, method: String, + paramKeys: List?, paramValues: List?, isOCSRequest: Boolean + ): ResponseData { + val result = StringBuilder() + var params: MutableList? = null + if (paramKeys != null && paramValues != null) { + params = ArrayList() + for (i in paramKeys.indices) { + params.add(QueryParam(paramKeys[i], paramValues[i])) + } + } + val headers: MutableMap> = HashMap() + if (isOCSRequest) { + val acceptHeader: MutableList = ArrayList() + acceptHeader.add("application/json") + headers["Accept"] = acceptHeader + } + val nextcloudRequest: NextcloudRequest = if (params == null) { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .setHeader(headers) + .build() + } else { + NextcloudRequest.Builder() + .setMethod(method) + .setUrl(target) + .setParameter(params) + .setHeader(headers) + .build() + } + try { + val response = nextcloudAPI.performNetworkRequestV2(nextcloudRequest) + val inputStream = response.body + val rd = BufferedReader(InputStreamReader(inputStream)) + var line: String? + while (rd.readLine().also { line = it } != null) { + result.append(line) + } + Log.d(javaClass.simpleName, "RES versatile $result") + inputStream.close() + } catch (e: TokenMismatchException) { + Log.d(javaClass.simpleName, "Mismatcho SSO server request error $e") + throw e + } catch (e: NextcloudHttpRequestFailedException) { + Log.d(javaClass.simpleName, "SSO server HTTP request failed ${e.statusCode}") + throw e + } catch (e: Exception) { + Log.d(javaClass.simpleName, "SSO server request error $e") + } + return ResponseData(result.toString(), "", 0) + } + + @Throws(IOException::class, NextcloudHttpRequestFailedException::class) + private fun requestServer( + target: String, method: String, + paramKeys: List?, paramValues: List?, + lastETag: String?, username: String?, password: String?, + bearerToken: String?, isOCSRequest: Boolean + ): ResponseData { + val result = StringBuilder() + val httpCon = SupportUtil.getHttpURLConnection(target) + httpCon.requestMethod = method + if (bearerToken != null) { + httpCon.setRequestProperty("Authorization", "Bearer $bearerToken") + } else if (username != null) { + httpCon.setRequestProperty( + "Authorization", + "Basic " + Base64.encodeToString("$username:$password".toByteArray(), Base64.NO_WRAP) + ) + } + httpCon.setRequestProperty("Connection", "Close") + httpCon.setRequestProperty("User-Agent", "Cowspent/" + SupportUtil.getAppVersionName(context)) + if (lastETag != null && METHOD_GET == method) { + httpCon.setRequestProperty("If-None-Match", lastETag) + } + if (isOCSRequest) { + httpCon.setRequestProperty("OCS-APIRequest", "true") + httpCon.setRequestProperty("Accept", "application/json") + } + httpCon.connectTimeout = 10 * 1000 // 10 seconds + Log.d(javaClass.simpleName, "$method $target") + if (paramKeys != null && paramValues != null) { + var dataString = "" + for (i in paramKeys.indices) { + val key = paramKeys[i] + val value = paramValues[i] + if (dataString.isNotEmpty()) { + dataString += "&" + } + dataString += URLEncoder.encode(key, "UTF-8") + "=" + dataString += URLEncoder.encode(value, "UTF-8") + } + val data = dataString.toByteArray() + Log.d(javaClass.simpleName, "Params: $dataString") + httpCon.setFixedLengthStreamingMode(data.size) + httpCon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") + httpCon.setRequestProperty("Content-Length", data.size.toString()) + httpCon.doOutput = true + val os = httpCon.outputStream + os.write(data) + os.flush() + os.close() + } + val responseCode = httpCon.responseCode + Log.d(javaClass.simpleName, "HTTP response code: $responseCode") + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + throw ServerResponse.NotModifiedException() + } + Log.d(TAG, "METHOD : $method") + val rd: BufferedReader = if (responseCode in 200..399) { + BufferedReader(InputStreamReader(httpCon.inputStream)) + } else { + Log.e(TAG, "ERROR CODE : $responseCode") + BufferedReader(InputStreamReader(httpCon.errorStream)) + } + var line: String? + while (rd.readLine().also { line = it } != null) { + result.append(line) + } + if (responseCode >= 400) { + throw NextcloudHttpRequestFailedException(context, responseCode, IOException(result.toString())) + } + val etag = httpCon.getHeaderField("ETag") + val lastModified = httpCon.getHeaderFieldDate("Last-Modified", 0) / 1000 + Log.i(TAG, "Result length: " + result.length + (if (paramKeys == null) "" else "; Request length: " + result.length)) + Log.d(TAG, "ETag: $etag; Last-Modified: $lastModified (${httpCon.getHeaderField("Last-Modified")})") + return ResponseData(result.toString(), etag, lastModified) + } + + companion object { + private val TAG = VersatileProjectSyncClient::class.java.simpleName + const val METHOD_GET = "GET" + const val METHOD_POST = "POST" + const val METHOD_PUT = "PUT" + const val METHOD_DELETE = "DELETE" + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/evalMath.kt b/app/src/main/java/net/helcel/cowspent/util/evalMath.kt new file mode 100644 index 0000000..e81103a --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/evalMath.kt @@ -0,0 +1,21 @@ +package net.helcel.cowspent.util +import android.database.sqlite.SQLiteDatabase + +fun evalMath(expression: String): Double { + var result = 0.0 + var db: SQLiteDatabase? = null + try { + // Opens a temporary, in-memory system database block + db = SQLiteDatabase.create(null) + val cursor = db.rawQuery("SELECT ($expression);", null) + if (cursor.moveToFirst()) { + result = cursor.getDouble(0) + } + cursor.close() + } catch (e: Exception) { + e.printStackTrace() + } finally { + db?.close() + } + return result +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..724b0df --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..030fd29 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/test.xml b/app/src/main/res/drawable/test.xml new file mode 100644 index 0000000..1828a39 --- /dev/null +++ b/app/src/main/res/drawable/test.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000..a6d1f9c --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1,2 @@ +# https://developer.android.com/guide/topics/resources/app-languages +unqualifiedResLocale=en \ No newline at end of file diff --git a/app/src/main/res/values-night/booleans.xml b/app/src/main/res/values-night/booleans.xml new file mode 100644 index 0000000..41cc6fe --- /dev/null +++ b/app/src/main/res/values-night/booleans.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..4ba5bdf --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,11 @@ + + + #72D1FF + #000000 + #72D1FF + #121212 + #ffffff + #121212 + #ffffff + #aaaaaa + diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..b479681 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/booleans.xml b/app/src/main/res/values/booleans.xml new file mode 100644 index 0000000..c47017c --- /dev/null +++ b/app/src/main/res/values/booleans.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..dd5894e --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,14 @@ + + + #0082C9 + #ffffff + #0082C9 + #ffffff + #000000 + + #ffffff + #000000 + #666666 + #00CC00 + #CC0000 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..1d514eb --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 0dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..00ff0c4 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,282 @@ + + + + Cowspent + All bills + New bill + App settings + Scan QRCode from Cospend/Cowspent + Add project + Save bill + Edit project + Share project + What? + Who paid? + All + None + + Bill edition + Bill values have been changed. What do you want to do? + Save + Discard + Currency manager + Add currency + Currency management is only available for Cospend projects + Comment + Convert current amount from another currency into %s + There is no additional currency + Main currency + Name + Exchange rate + For whom? + Project ID/name + + Repeat this bill every + Mode + Category + + Label missing categories + Label Bills + No more bills to label + Skip + Suggested + No suggestions for this bill + What do you want to do? + Where? + Local + Cospend + ihatemoney + ihatemoney address + Join + Create + Nextcloud address + Password (local) + Password + Project title + E-mail + Warning: creating a Cospend project using Nextcloud account works with Cospend v0.3.4 and higher. + Authenticated project creation + Nothing was changed + Project successfully added to Cowspent + Search + Delete + Error + Close + Loading + Project sync failed + Synchronization with project %1$s failed. Error message:\n\n%2$s\n\nThe associated remote project might not exist anymore. If so, you could remove this project from Cowspent. + The Nextcloud instance is probably in maintenance mode + 400 Bad request + 401 Unauthorized + 403 Forbidden + 404 Not Found + Creating remote project + Cancel + Ok + Yes + No + Add + Edit bill + New bill + Add project in Cowspent + Edit project + About + Impossible to share this project + Share + Create bills + Settlement bill + Share Cowspent link + Delete project on server + Save project + Add a member + Remove Project + Project %1$s has been removed + Manage project + Manage members + Export project + Last sync: %1$02d:%2$02d + Scan QR code + Choose from Nextcloud account + Import from CSV file + Malformed CSV, bad owers on line %1$d + Malformed CSV, bad date on line %1$d + Malformed CSV, bad column names on line %1$d + This member already exists in this project + Are you sure? + Are you sure you want to delete remote project? + The remote project will NOT be deleted + Activated + Name + Color + Weight + Delete member + Remote project operations require network connectivity + You are not allowed to perform this action + + + Appearance + Network + Other + About Cowspent + Nextcloud account + Logged in as %1$s + Logout + Server address + WARNING: "http" is unsafe. Please use "https". + Username + Password + Connect + Theme + Offline mode + Only sync manually. + Custom color + UI Accent color + Choose color + Color Selection + System + Nextcloud + Manual + + + Main currency saved + Project saved in %s + New member name + Edit member + Optimal + Who pays? + To whom? + How much? + Total Spent: %s + From + To + Who + Paid + Spent + Balance + Statistics + Settle + Project %1$s was successfully added + Project %1$s was successfully created + Project added + Project created + Statistics + Settle the project + The expenses in this project are already balanced. There is nothing to settle! + Share the project + Scan this QRCode with Cowspent or a scanner app on an Android device where Cowspent is installed. You can also simply send/share the Cowspent link above. + Share this web link to let others access the project with a web browser. + Web link + Cowspent link + Share this link to a Cowspent user. + Cowspent link of %1$s + Share Cowspent link of %1$s + Here is how to settle project %1$s : + %1$s owes %3$.2f to %2$s + Settle the project %1$s + Statistics of project %1$s : + MEMBER_NAME (PAID | SPENT | BALANCE) + Click here to connect to a Nextcloud account + At least one activated member is required to add a bill + Impossible for local projects + Choose a project from your Nextcloud account + There is no Cospend project in your Nextcloud account + Activity in project %1$s + + + No Projects + You don\'t have any projects yet. If you have Cospend installed on a Nextcloud server, you can configure your Nextcloud account. Alternatively, you can manually add projects. + Configure Nextcloud account + Manually add a project (Cospend or local) + No Members + This project doesn\'t have any members. In order to create bills, you need to add at least one member (better two or more). + No Bills + This project doesn\'t have any bills. Click the button below to add your first bill. + + + + + + + Impossible to reach the remote project:\n\n%1$s + Project sync failed: %1$s + Invalid login: %1$s + is the Cospend app activated on the server? + server connection is broken + no network connection + server error + wrong username or password + Invalid e-mail address + Invalid project ID/name + Invalid project title + Invalid bill name + Invalid bill date + Bill should have a payer + Bill should have at least one ower + Error at remote project creation : %s + Error at remote project edition : %s + Please reauthenticate your SSO account in settings + Request failed + + + Version %1$s + Maintainer + Helcel.net + License + GNU GPL3x / CC BY-SA 4.0 + Source code + https://github.com/helcel-net/cowspent + + + nightMode + appColor + serverColor + ncAvatar + useServerColor + useSystemColor + colorMode + offlineMode + showArchived + Light + Dark + Follow system + 1 + 2 + -1 + Use Nextcloud app account + + + QR code could not be scanned. + Manage currencies + Show archived projects + + + + No repeat + Daily + Weekly + Fortnightly + Monthly + Yearly + No payment mode + All payment modes + Credit card + Cash + Check + Online service + Transfer + No category + All categories + All except reimbursement + Grocery + Bar/Party + Rent + Bill + Excursion/Culture + Health + Shopping + Reimbursement + Restaurant + Accommodation + Transport + Sport + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..4a8d63a --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..ef899d4 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml new file mode 100644 index 0000000..257d8f1 --- /dev/null +++ b/app/src/main/res/xml/searchable.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..696a913 --- /dev/null +++ b/build.gradle @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + kotlin_version = '2.2.10' + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6fe660e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,28 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +#org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +org.gradle.dependency.verification.console=verbose + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +android.useAndroidX=true +android.nonTransitiveRClass=false +org.gradle.warning.mode=all +android.uniquePackageNames=false +android.dependency.useConstraints=false +android.r8.strictFullModeForKeepRules=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df6a6ad --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt new file mode 100644 index 0000000..d045db8 --- /dev/null +++ b/metadata/en-US/full_description.txt @@ -0,0 +1,36 @@ +Cowspent is a shared budget manager able to synchronize with Nextcloud Cospend. It is partially based on MoneyBuster + +What did we change from MoneyBuster ? + +We added more statistics, support for projects marked as archived (hidden by default), +added support for custom splits to bills, added a way to add categories to bills missing them. +Behind the scenes we migrated the UI and app into Kotlin and Jetpack Compose for a smoother and more modern experience. + +What's different from other shared budget managers? + +You can keep your project local or make it synchronize with Nextcloud Cospend projects. +This means you can choose where your data is going and preserve your privacy. + +# Features + +* Manage projects, members and bills (add/remove/edit) +* Search bills (by payer, name, amount, date) +* Generate and share project statistics and settlement plan +* Dark theme and customizable main app color +* Share/import projects with link/QRCode +* Connect to a Nextcloud account to automatically add projects +* Background sync service with notifications on bills events +* Multi-lingual user-interface (translated on Crowdin: https://crowdin.com/project/cowspent) + +# Requirements + +If you want to host a project in Nextcloud Cospend : + +* Nextcloud instance running with Cospend app installed + +If you want to be able to create remote projects from Cowspent : enable public project creation on your Nextcloud Cospend instance. + +# Alternatives + +MoneyBuster which is FOSS but has older UI and was missing some QoL features. +Tricount, Cospender, Splitwise etc... which are not FOSS and force you to give away your private data. diff --git a/metadata/en-US/images/icon.webp b/metadata/en-US/images/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..975d76a9eaaf18e3cc5acd56e84e067aaf86ebae GIT binary patch literal 15418 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 v(W1Ndeh1Fp^KwK7v0GiuhX{ly^7^bIYtKfq= z+TXSTEN!FqfA4rSFALFadNCHU3=Xla0%+sW?@Ch=pC}tji%rFbIK_rW9{{%ssR!Kq z1U%(70D*2IOsLhWiIsCaHh&CfNm^N1ZM-K1vWGr4DumW`5q;y)y4(VMm#Yc7eo}%A zrW+VEvT#*2hF2mh6h>7Cfzynyqa$^xEvtz?a>*YpXfJ5{?#Fq{|5dRNY97f`6K$fY z97~&rvSH`D{_rjB6&G8>IstQipkmOZ0*WO%BAGQHRFQ&XS?3}^sPf^eQOHUVKDvs^`Vsd#;8zgE%Qq=3CP)t%b7$y&N8x6NN03to8O@0*+A^!KB zy<3@SV2=WS9u#D?>iNxZ3+Ge|s~9{=%1h}=dKqNHNM5I%A{A5C@PpBolBY8PW!lde zO#~gpf2Wo=oDYq!)1n1@Q6I1X5_B4pkV^X02qrk2N79V zF5qJy-=!wwH$F$>1YZ8q7J9!G>&^Q2cHLoVETv~)pHy@m5lYF!_t40o>l-kUZHbI7 zB*1~AGA)9O#X(y{M#QC+6o_>>rIv5Onv9XR zrSGT!mjdM7g`t#$VjoR9xHoor*{+e0Xi}gUKs;W5QQf+ln3K$6OvHWdzU_Ocs%6`ZFUXBIREkSXJ5K#Qn#({n3>(DQl_ zp)w!-fn`hU5x#BVPf zd^(dr7eJo@J+@c!Os4s0$7Jsm9uJLI!x)E;9z@S2GZx{U;x2P%vOB5)MBJ!pZhym* z$2UOwx_X^4)&ZY;aPcyk}ui(Jdc|Ka)}@Ma1GOEVYq$^0SnS2Atv; zHfm`Z51#>krKbRlt_z497hYZe($`^yx&>?pMjbSrXo7A`z6fTSVcwbX3X5}e3=BQJ zf0YMy*T_!U#0^xB08{TUdX^)3$bEky-)103-N33NLUIblVD^+6 zJi(?0*JiJgGd>p^Bn|i-eijv%Wi|b3>=BK( zhVe+vdBPn)m0i%N;srQZw*Wyz26&lb0D5h2#PkWQ#%_!z-0zVA&fn@04_KHGEzSQ14kx+YQ=*PNY!g+xiYVhNzOY7O5ILB|DAe> z{c{p?*beJtPGG&hJ)6^8hjlkkB6@w7Hm45{>*S-(xbuIoJj()KyX@CVF8c^yc`k|_ uoXPw4fT{4Sp~-;%2xI*0h4bsNCl@~KCAHpik%2;9B6XG9%4JIT{r?B2I^0bF literal 0 HcmV?d00001 diff --git a/metadata/en-US/images/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png new file mode 100644 index 0000000000000000000000000000000000000000..121f970458c2dc1a66912bf80d14e31929f9d629 GIT binary patch literal 117522 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

XSut;9kG?h0gsH%DX)?11|0&4-l`Ja?_p$I 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 literal 0 HcmV?d00001 diff --git a/metadata/en-US/images/phoneScreenshots/3.png b/metadata/en-US/images/phoneScreenshots/3.png new file mode 100644 index 0000000000000000000000000000000000000000..68b9670077eadc815fe50ac13a1c2c1a61177a93 GIT binary patch literal 119001 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ⅈ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

    HYw?@7`3LX1BvVf8PY>yDW zrs4~QRiFAi>pn#(`ih{)Ux7&jC=ME#sYnS5txJ($c$e6tqO;vBU?R77n7o-g)5ECP zA25imvv8eFMjBo+BCBj zDfwC7fCbpK{Ui;7UiZcD$xLnESY1LYFo0S7{GE`*U*x2bi%>hYq?^tMmm2&;-su&O za2eiDW1IccT0|NIJ4gDY@2yQ<4P5GA*p9(R}ztXwrVQ8uZXX7YMVdKPsD#p z;WRrPVnJwCI~%K^NHR6@u3a?!E8?$TZ1@7vC4yx+Q(~xhp77_ss=C1Gu4Xpb!Xbzq zhFEpB1~p)qoIlxj-&J)GCq(}>8hvwSKv5Y!jCcCajGl!v-x3%kY>kn>2K8O5=Zi+@ax)!6_>%g!n(%(>xf zzn@6sP%rAkei#L5FBG8By2S;-B_xh-^_hz8xoJi=()&=$Y+?6!PlZ(^4h;AJ=}VNP zx4RFLVvSVm7MBDUkT_a|k_eWXH;wx&1@XcpS|O@KBKu;$qsHl zfY-&w#0iT(SPlZ*BJf<3Nh)*pYohQIkyZpGd>Qc;8+9fw5WG;hu5YUD2=lO+&AGyG z^?BZzxIkb{JLjYZXQ^f-Uy-Ra?cI@yl6~&8y+RL*e`0dDqa+Z{Ng5Qd*w0>yn%?}) z`loNvc_1B~d>F62zB`fTGjv9g!b0(d@~W=_S@nRnrdKwF&TRkLnIuD(^h}@Fy)f5y zHNeKyT^gCWHgmRJ_Qi+2Xf`n^fF`AkG}K*6Q0Txtk=A+M9^;vBy}9h{nI3?ZxOl>* zVj<|P!}_H)FM-0v4QW8C0s5286w;h~_BE8TEsvrA^Q*ZPahaA6@M1G_?P_@K-d(vC zt2Oi?jVxfyR!MQtN7%jQSYHuZlU067+s&!O^B~Y05!u!y)u`tde%9Zbyd@IGDmGvN z-oSBI_+n^ml8Uixn|>vMuqH8IleqOhB0D)E1nDB@Y%*O>JO?aC2sicN35^)4X10i5 zfL{wGWXp+6-H z-HR6CQGE;8y@t!MwuUGWC|k0Tp;Xhg0!_JPU&GwhK@QS|&?BeHk$P1_Cu@@HzyS^& zgCXW^Ti67;=>hh;Svk@O<^36&J+|>o0E>}*1j5u4kgIBc%mppKTa=Ycy!DvFA;0}@ zt3OxpVf$^9$KMRH=1+*R zjz;3@IYI5w+Rh33v2p#J{GjY=%mF`sCG%jX?HBgXp+rM`$ofKgV!`QQ1!IFr>n|gG zPv)-ZzTbG;H<>yYgm7z%c>PBS#LYg6F6<9buJP4@26GGIC7-N-kaXgEuHZLB49ihi zK#vS|QI0q{I5kVZ-!K?75VYMUzhX&DllZ!P)K}324IZ?ev1mKQ;*TcH{njW$EepJ> z!Z$|Q9(S$$#idXvlwwv_H!$H`f8F+RP`LqF92ujB31b_|yufJJ?N4dzIwxK)-uE-H zcKIkW09k6ad_uf@>g<{muD*||@Y`DpU#w}y*4KH?l~vRDeBH>vygl8`_fC=V_s3Vr z4Q{wtZpGE67kzfEKneirldet5;I!J;R_0luUR2ll8y#8j4Egx^)z}HV1ZFJ^&yuzG zWWRHk$g@9`a+E~Om^}&Fk13QL4w~d>9W$>}>-Px2_su)cjAQoX@Wi&wC``ra=B%fy z|AP65F3$_=_|9C)+|NW^P|hiv=O5^j(3`7k1hA##|MsJ8J5#(ILkJT zLUvZ?PLA+aM5K$|$>(}TBnmmNDp_2NOu?R%W54*&B^_*dHK;VRX^BaTE^q|47{;!K zoKEdvLXL>k>-)SWw^5(H?9sg&Cw~nxna;7A@{YdLWqmr1rY)(>SunO{@|i)apRewW zg$C~1#-(TBq6l&IO^vdvt?eDY*x9kbHZmtSP_niqBsK71Z>Xj(`x2{A#w9d99QVrJXskv%)|yE%;192f2|e_2Dvca5#)G)-Hy{*XO@ zh728dY_|2b9B8@HPhC;HWdXwb?GwM3Q-B)v>b-BLUEzo)hZS_ES_noOyJzE;QqVIfB(Uyy%$j@%J*%Vjty|{ZO}@oqn65 za(t0@$6i#Wcw`n8ERm48TdE7mfS;#K`!aAh?TdvMGqZNdNgjj5hH&n03F+cNx+cx) zJNZVeOxh(uTepP#)lqrBPiD6{I3H2>JIF-rrF8zrdFel^JS*396&n%WT4w4Xl)$LH3(Lh(A0y zlb~C3#o%%2A!Xa1;=Y)bpOCB7&QMTrg%)3FZOMub(+9HKbR-70?M1u)_BQ)e?x7No z-1L*~96vWXrKKgyH4w;A5E)BVLDUB{2_5wr$osFth6OL@cZNvbG5k^?F`d!++c8{$ z=hMkicZlCiP6^!S=;v^j*HN0Px*iwUn`5*n$b2FtnzziLR(Aab2&Fak?7?AO&W_C> z6X-GR-PYsj)_ywvgu_QL@AjP515V;cE_zPu5-OFlL|RnI=;z>lOM*#-XUJhs2M5}x>_5SpA4I@nQvWk@3vberuU|!-H@=;5;I1=kEIsV zgsJX@13#rO`!bQYs;(wo1}cScj@KtBM!ETs?PYH`Z}QLen_q0Q`c z<7ycXo!IdCTjUHNjCO|O)roLF^Pz+6VLi}j(NZKmUuWfYm%k(S)R5VoKU{97+k>b6 zfn4U!LvEEJ`}GxGZG@R+>W;4C)&p+5KsriYr@@Zu+N;^Uz69Pk39;^uAfk{GMx7DLz`AXH0oeNrK@us z9@@$W9i>T29Cn(Aq}IpX%|%pQbOYhMTdD+btehcDf>|xmCu8-LTe_INQ33DIv7nBL*H5A(4?UM}23!IZTxeU?g2Q0Vt=5bK%uMyzFz( z#lwOCSri?gqK(LE5ul4z4<6^kX_J=kvL|137^KmzyFo`~-qolh z8YcF~OH2ye4%XTpJz$hSSdd@HDAXR!EvUxEM6&V4`%#ZN0oEewH(tzVgZ>fhUiza6 z!=BQephF+^fap2*nzC4b2l~Hwy=`N*r;Ln1P=u6F#~!nL(uM< zu#<!GnR11CDh%Fs75U^%88zNPCrPUPqIQUobAdRYC zbb9doa`0q;Rhhq6)#5kY$m_YHjN3fG5v2lrjB>Fd99MM5bdjEN=(j~-N#&h;sJ!-v z8_Vm$4%;p!CpgB_-M>M!)Cp-lfeW%+ZhRN+noQ%-X-SdSYe&oJcgC{*7m(st6%eXVFiwmWYg+sT$@)(}r3q^yE(B z9!z>rXUnXd7&C5-j2zf<5g)tFvW=&x0-|ToGyTQQc<8cU1brZmyI@EbVewshrp-(D z^5N2^Z(cy>LKZmYbCLD$gTa6>5!OvPlq|WsQm4Za&?tpD8_A z8pT^|`o|MdQF7~Y0JxnTzNnd2F>WNHOz9k(n;Yb+GR#tHQfNshshk?tA}9eQnJxyl zrC72Wjg}mRPxhYyH|b=;4Xst@Ni<}4$WI+P53k{RH+!%#8(*fLtDQJvxX8nGfZM79 z(>+=6E-m>asIRWXS1c3{YtIQ*7f&`PhSP(PsvEIWx^F3P!Ht8OX$fieK2O_oh;!*& z+r8OsaVc%<6_^kJb53@ZIp`x-+7A0osE*;V7Vsp>rJBv(YqdL6>0jyuftv*EK)pVX zB3UU3-(fdSaHsz8rL9sUJ01BYZ;rC;CDD@WEZQM|h(a#{%uQMre-9l9ouLXU`NsKP zzBEIO?&w;LU<4Z>2;R5-LnnCE_khcHWFfpj$Xg18GhB%Il<;;ST{Z>O7^S-KD9v*F zdC0tgP{viaqT~pC`0Nfoq$Xa*{h|aXwNI& z!r_|luM{RpLO6R)CiAK_;HgKaM|Su%ynoihc#6GGc$~D6oxmsNOGOADmeQq8d> zMC#GZ)i?oD<7;}NHXlX)<}5oZl-pZb*e@23imW(1O2^|ye}7%rXk8kq0(VN3C)0pD zOFF`sHXCDBJ+bQz=TNe(8|JQZt*6zAoF$Ws`NQN4m^LWaZ!z1UL$Nn+Fy4s0)sa%N zsH$+@5xG74-^PYLxq5rA$kjOak z<7HPIdWhNYa;F$^+cTNOq3PMt;7cfm$l+6{KlU>E8KZLcqbH(q9coSV7F9#CX(9l%e;;_9plMaDmTf5=+ucO#0d?`U}<|)V!C{AkloH)$=e`F@}_QP*<$wB9`vXJA&^Q^lBTCWuyB< zb&B5(WXTqSrV`V*Pj`od=f4VxV`h(kRcEq~;A{Iegw8=|{ess{k1X97IkdS>{y=9O zEsqvd@1l>-Zevcp|$QJZmi_aJt$S;QZ&rZOFg&1kR|%o&(69QS!sh z2}ci(W;QXvf9-l#-6n-TlKuMCvvb%f|M(mv8+X`3Q#js+kHxMb?V$M@VmBwcAkx^xf!OfN}(g0dH-E9ro z8ZeO1FR-paO8JSvhqP-YZn)llR{%Jy>zm%A5S~w>{%?Y02ilI;i1D*K(2&1{*tN3k zx9XmTETeqIc0ZksObz`xBH30w79!gAp*?HEc}o)P{m2SSYEYT5@W==5G5@#$gls)% zN@uD@IAqMcHHAFR1yyQb)}8|vB|Pvdr5gS9$9^Oalj-|^OtT~*BJnU6^_tr|wFoM| z?f$+@a=%M{_l9e#v2NlAD}RDVJh4v~w+-ZaqwL#a$jJ>yJl8Byr!el;C+5H+j~mm3 ziy&#Xt}bz^u5R)SZs+TBMJ_%&8oJ_Nfkf-hUxU-Se&&@0F5=hYkN!{fcaCs=x;y@( zGq`*=C9om?X!`p}h4h$Dmy!VNXD}!|O8uRbx8dWhc7LT!moH-QCfF5x;o&FUuv0-R zFCkZ{gR|QP98^d8hW=iws*z*AQbw#Q1+7>L`nHg1>GwNxy0h%=!;h@CO?F$zve!O< z<%0uthWu)luPPAL(`h7!iVxnTd7LgDlvTP^BtgGIAhZh3uh98{^$ z|88A@7TdOeHYUhZsQeG0TBgA7qYO3;JkB?1rZrC|-ICub)NU~e(mC4WKOH*;?lRH> z4t0*314vgiEi19EQeqE2!vkhE)46_?l_q_-UOEP(^JCrpR)lfL{&RjBCn>CR zv`I)G@83F+VRgt)p1KA?;c?bAUvMyHSct=AGN`cZ?|>}F034tf-u{{o8>4wRy{itD%~42bUJG2e4c;9gI!%|rw_POit|{fHB(ErO z9aaWR_ylLw$6XIZFNgS{?!zQkqD+FKXAZw}G}|FWJ(Gg77Ya6mC-#R$DHX<3A?91< zgS|S-!l~^TRggxB(i0kD;uguP#a^Xn; ztI7SK4jb?`*X-&l|AK&P@bp&jYSeyGg|E@Q-=!9oqI_?HzizmfM81jqssU!_07;vM zPnG<-x-nM43bLAFHc`7VSEMU-!?~pP(~zdn$#BXPW^Hw)Cu{q{CS+IIj-CLvpm0?+ z%GYm8Po3EHJ|{;XQevGub|L}R8Xc=y@fy~*~hp&~F^}yKu*0@O~&E zL7d6$1~CjOi{(5iUZLhXZfo9WR;g9@xAR{!$7VKEbXewoewZ5GqW%0gvIzbRHZ^@d z`~5DLMOVXWF7wz+bRO#0@hM^RA=mX1WKJ5}D%qh}MX<%i&{v5kCvQ?tW1HOYJI!7c z3iYfUnOo)Pf0d<@Vf$Tclh_kNNWL=qM=LFOU0r74U6$N%H`L8@6@CGJs;Q zMnb@*Dq{CL)oVrN4u0q~Fb2+rlc1#zDn!k5gjI~%zr(c!#j%c2zD}E68oSi?mnU+d zP__p&)gOdEcJA2^M6djFH-_RmnU(+YmYRp!5%c2ACNpBRAJ8GawwY8=v=JLNpmTb3I*1Kn$GV2yK z=16k}0U+w~Id-(Vm~?K)>3GONeYTRG82$Ab5Nc!9w=s-$M3+)JoHMwEAm-|p_4n_s zm%M$Ws$@@0C0RUMD-;)doZ?J`mQg178FF>2-?K!k-b-I^hU3o4-ePfKd2ouY= z3>F?se_u3W(r6dsOSE&obrSv1M>qC@7i)>2a!B@sS%U4_^my zfjs#S=U{ga=Rl}TC%?j!36C&KVUbq0XBP?n77-FWXkEXV11q3#qI*tN&*5|5ZN3vf zDo_nin?ej8aiP<)qV$jbjWmeGixU<)5Qy1^l$sm!$YU{JDAOmzwOV9J9IgtHHR2;OjbQBwde z#a>`U8UQsDop1^stQULjFex&CP;^J+VpBTwNW*5f%=wP<%OODZLIDkwewx?B;e|V} zK><+q_+LuLnkaBuv9HJjQ5`Z+w(R``4cmJ!X+d+ZtlCw-z*)7r)7>?SM>dZbAwcni zLK^IRL7J5k^!Mnc_#1>%r)8V1uZVVzA5g{HDASaOc->*V5U|V)3cVa`!(LVhOz0v_ zNQGEUHE@i`%t3#aJ2x|h2CRDwbSk2)pNgp@?CxPJe#_1#3!p6}{$uwL0z^kJ?2g-z~a-36*xThd32nK1p7q_HqY*VqqgpC1pb&-Qmens8y>1!oGpx9-W z?`Yqy@IoTgEU3*kZ9JUJ+> zU+4qEy=1p*9y!;^2vVR<=)QRv*E-ic z;Xw&f@c2vVA*!k&55Zo@bOLCd9~t?;0M!{j$UZL!%C0@?lv+*EHy=&!=;i{23ch=> zwWC6tYjyL0UJDf8heP^|NeN9Q;M%x|hoGZKJGBT71$n`2KM7qze(le7JC zM;_1?s@T6pp5YJ!82cb;ai8G2Sli&h+OBSxS$ z>M8AY9m~%Ry*ZOT>>Qy<$%(A z#2)U{jb;CIWNXavVp8ZDF!g*1<$aVq%$0d6fgn;;%j5w8g)%H$ISPojf6 z%9>@3Io4BxaI%&~Og(hxrBkikpyEKQ1v=nupA{hS_*OMi0lh&pzO3N~I)6^p*2!1g zH&<9#)JyYpEU={N<{Prx9b&C!P}kzs{tpK!1ZER z6*bR-)x+%mZRB$MYXuxj;Cb^a3Stz_AS8;r5j@W^`H5Ci)aCQ3-C6BF8y4J)Zh06* z+OSVFj)K7pzZepIwhTrc+>XxRJ{t(Wl%7%h z(>Vw@4SBkKwicy*Je)iRd;&E>3F#6L+uNbC(p`+pPB_koRGL#v9vBmo-%id>?*+!MTV){@YIOlCLl zW)%J9O9q9$f&_DO{wRoVjp`xm`N8rnSnq5OOqAXD_aZgeokp_J>Z04(X{HqJ-99eRPEv0DHD#{n4 zV{`3AYwT=k+y8!yoQxX$5aYTV%YgL#Z^0$ES4(sk01A1*-B)|jj{zq<4zvxe1$CT} z0*e?Xi#jjW49cB6vd(?)lmm$sdBA!O;(=uWJSSip#(yZ{)OzPBFCv$sF2z2hTZBRA zvxf>3o!YF=HmG^Y<)W;}AQTFtl>Z20HopyBh`FHM*a|enZPJ~!e<|=68HI7VTy)D8 z2P@>ezVQ6TwsM3*JE)$6o1k+Pu!FQT}8eY0_`JqZq9gx`j z#^)uC|5JcMdzdOKd!|wQm!R_9_Nxm;6us;EmydrbtMU2&9|vkdq=yy}ifAEiR+J*}5pn;2 z@`^Z55$D9+^MH){64U?YDFL$Lo;zE^%nNpV#vs*vF*o%nG}n^E<-bJmnzW;bfDhkZ zwWC{tT>#2Az@uP8BZ<2GFA)==2>;&uI_+VP|9j&W9!lZ&?}u!=Vt`_GL_`xlzm5;@y01WVbnHS`G`F8E<}BBZ((e`)OC~>KW-zncSG4 z4wwBb`zo?FjQ@qLY;vDic4_<=k1h3rS(p`$ZMXul5^yz%^x`y>C~)!$B@V4TBa< zb8T(ljYI+xFGSjbxtof8R4LAnoZ;K`Iaqc?v6oM!2%eH|)Tw;8p`(fmo_B}Q5c_;Q zsk?*VTUp_KddAeKQ_;irux1)bu)^t9!7bjI2u!e$W|6%O!PD)&oE%6QQWt;!!;$Th4R`x4D_61Q!QG}*M9^64^un1EjKAclYW<_c zyrQUo3~N$uf8{t{&Jo3sB4ovIh@P04 z@#N>{cX4qU(9+Us=e#&&dsu^5WJBs>iAfgLXoW-*?Wr*W?K^E7@s;lS@tmT4qj zC7S67&T$}rQz3-}11{kP& zVDTh80ZQwL1pi}=yy^JPRK1EWRChz-JA|p22v}WArZZS(>G=_Z?jRQjB5qVZn6w0T zmz@-T0Fwl-m+J)Bgg%i8@qd=EnBSdk`kMagn}Sy9k$u&#S*z>2t6?Dza&2pS&Q4RT zy%Ie+!*7*#CQ?oj%b?W+WL~8HXCO5LT93b#-8GE#iKU}t@a9wc7C9fLsiQ-UG#=rK zqGvZ-9Lj&>;^@eadm`HC8osD;%1w|Ity#w!Te*EkbS2r2OQ9N2QmJf(JZJigwN;`o zWv7Vy@kK>KzG>AsS$t--ufW`D`UU&iG_-E!aMk#ptDge~iTr%7f9mCfUrcV_P$9W6 z){Ds(*eJYR4txtUy6n`iDd z6BUHw_@;!IIL`h%HRc;^GWG)fSN#>wFsswd48DeAknR3$J8R35uFI z8X2lodoP(~JRA(R4Anz^ubZAJI%<$?^ET8*s6sE1$FUxgm)?_qjQ4qN{9R|#l+&lb zn&;vCEEPw8H3k0HfO=?Uovx|9r{_01PBXiwPajM~<)?-%91qEr|D^vKrlyrR^nUAo z6(A_@>Hfla(@1LmjD%I{@0ld4%;xx9@yRPi&{EyY_oT|)bYc(t_SXbJ)hpeOGgx=Uv(3d7s#6>iWs#T&thVm34-9(Z{G#TG*u=$Av-9ymGo$?DGqt7T6efaJ9xU-oD`EoG_gXeufGh)4h6s?rASQ|Pl8sTr z+uj_vBk4syYtu_6Umeq|1kkf%X}XKMivUj3P)yc7{wLS5g_d5N3}@_lNkxxWkiv_A z)v9sZ&}8$n*R^LUKkQlmru(Px<@<$q&zuJ=Bz<*-Cb^?%&rZ&>oq-$_a(bDKBr9Yg2V}#jZ^bEt$Wx6PbN6+brW03OeIN=;5Kb2QtS8U$kp5lL31X*u1(3^^mevqYL08{piFl8I zDC}XFc0s!8@#$+1@03lMP(s~aj%)KQtzUnML?C#n5TD#|QQ)m%nFLxagjjINC91l&(;e z&}+A8Ti_;_1#LLFyqMm|YVn{9D5gj(uvIrRSJRu9f6-4?%(&X`Xuo2dHy?| za^9%47Tveup*&+Jgs)92PqWf9%FjXIzjA`boaW_Y;la3;MtQ=+QfMyMSZUFgoJm-_UQ`m(=w>My zBE|aj^VDT>*~zhPv;D7ADMgr0b_ z^FI&2rf8U`yHg#>&Ru2@HlAwU%6M2_wDb27O9dx}-O@(_qkH)edk3o8I}7z#991cO zXYR2@bVW;!_4P)8!Er@I;Y6s?NO7@69iSmrk3@F-cbhx6e_F{_$Ag9{_G$Sq~Uiu%{aZe(5xhB-kf{<(R{CyKJ{zGWWc1Kv& z^bU^y+T)*+m!e<3itX0;m~GEz^EkVJKfV5Ey`Y7fpp&`TSgRGY%d3o+8eiMsE(u~Y zy6+IS^okh`!^?`EaqE6-hT!WzGpBV0BE*eiYc?4ACrX6``PYfK@<)DNLN<$itJLyh%Mc*q%|*W7BxW!P`d-4n#q z2T&fau^i5f{8iUt5X0zNTZf0;=n79_GF4Ou+9Pf-6K&IeyM^=wv;l!7b7~A_O^8z& z{csaPMHF=Ew$?-$!V-`G}Xh8L;F2iu2P^ z#y13Ht9+dF&1&Lx+WOr_m%!2Jk4uxzDF`(;_I83?x)_1T_<<9Mlh5Q+h?g2c2}6N6>C(D4oo z!m*)z#oJ0xo*nkMn;H_Vzyu8or!7Ny3&7^Z=Y&%V?D z^x&&df+V5Y>w4^`r-N%-2eMWHWE&cqWKtjOB&GM3_0|k%Hh6KIQM(toss zmsUEZm~=>Ptc2)CmC?GaS8;Dx&D53XU1T0l_y5pIN0ifh_+)-yv43(3U!qifHw%#u zzU)4)ekn+s)^BLw%MHx#$|s!HD4zzc!8&0VQ@zGg)-RZ)@|E#_Vt`A3r`$EAENZkQeYFkC=0}Chn`Ai7rVf8JFB9+?4Ejb$kS;dP8l4y+`RJ$Be~W7X8nY zLB9>5S-xHUtu)R+HIpx*($^GDRAz`evTTMH_O~iOk0|8Cbg7O0WJ($ zE3tdA$I_}wH#8^e678l@qWe5fPN`OK_no4aWoKFyA;{FO`2CwH(!qKaYWu3q+><~EA2$(!*Z)foXD=m;Jr8d!Oo8mc`zCDytP=!BFw5rXG&tj z_ObawUIFwDjjN!ONt~YNPr`#&V=OE!JL3*Rf-Vk}ORD__9v%AJZDzxf$1Q&qDyZ8cCOQ$YDD}Ig;{Zoho8I;pB-D-V|$>kM;(6$$&9jOqm032=|O($Y7w$HS|ytlXPOW*>Dx|lg}Q7!AzSt z`HN)rKMQ6vEcWBI&bab6etyPPCRPYpS8$kf=v_Ho=lNwoU&VlBrpBJ$ z{n+94m~%=}4Y~hNw`{R-OlIDM-OaMpcZ5f42C&@B{wpPU8Buw2Imw|s&c}MA* z89R9jXjSJyN4jvQ?EQKB$Da6pCJXUz$?I(y3@7$mzQltv?I4*}1&Y zHKBH=$gir_(0jT5$o3C}qgs|R$HP1kd$1;|9;BkiFa2(aQgWlSZ0RDJyf9O>s4~;o zO?z!war%+J>k}w~MJUf|cLA*ejo5!n%L#5Xs00fMn2`y|;4L=G1xm-0_PW!$O&jlMd)0NWCBUDm>Dx$tF#J=; z5ew&VptOlzE4;D7%e~?N)klWbkb6wMYjzI`&!dD$tI|)yXL%f!rDzN;m9&)vRanU7 zrPt(bIJGy&aiWB$#Hu<)t#zwtGKpmnLF=ZZ)TfzE?t^zd=C40 zEpf0wv8@)Rkh>UP?s}}*hqR!HMJ2x1Q;Xf+qG`ro87J*;sRwz(9-B0A<>aLUN1!^h zuIL&kRJMG2ahN;~E}`TgAuG;In;#@TnpSs%Qr8{nI#ib^UKQuEn>Y}lKg>gdLV}(x z=*_rt3)6F5@#*%(hb>0K&?ct@4>8dog@u)Q1vBa?TJ92z%*{!&M=pZ=C8MdkDA51- z6)lOTmsI9>CoVnrG4t^@0Gd+G2+12a&q0`?ns_W9T8cg?5%~K&XXzR(OF|?ZW1c0) zr`aRlQQjTPJ{~_Nwoqmzs_}+k1OGfb-FDYSdUr4i_hfQsT-`JA>EOcFfz!kG96B$3 z`UE(VOBqoxu^%zyzi$k(PGOlw7r8903n$b5K+^@26wU3kLH}G$OlBKf^w@zc3T{5J zh)5Vn!9gY$Rl3lVb}AY9lXhi!Q+K3wyeDfyd5V+OZWk|zZ!}qmE4PznTNI^Q>FZA4 z*CyFGtLCr9(->^jM7U|1DYH-}q;Z@-xu3n|RHSFnJ|Dq^oQq;}AIT}Amr$M~e$jEeEaR^ZX)%?E-RvpZu#bi-ZsukIei zJ?wEAgi4%)sEp64m2$ryv5H6<35kM7PS`1_AG=M&sV+=4@tym!lN+RdBjWn%=Mg*< zcP;;2`LQ9lCAX5S4s!LpB*5P{v1PR;^&yy)w;%tV88N|^Z*=(v7Cdn zK*!tjh!-0C7aM&terk+sHnrXsdtSBqaA8sxVL$-@!6>C%MDc{8U)e8Jo%i_>Uape> z5NAT;c=;9(Nujp9_KznpwRbkDYb=`_hq2zGhGm4D2k$mn*ZpX`{4ZZo=lfb>A7N34 z9+^a7TyB?hMN6t9*@kF~9X9D4HH*RpiD|Uo+=C*O9q-@px|b#%SZ4X?5RzLHLwGp- z*85YciY!^wjZxuf$AO%5m!?laI%#=RTu)`hc>wDyrIGa4sRcty4pR4b(0ilhLT|v3 zG8^EuF-VRS1MU;IG{MZ%42f$>V6~gWz=AJ!H@% zo2Lsg5CEyt$8{ZZko03#h#Ba6l1~LK4@LAxMQg1_DO53kTKm$L@&qZoRwo3x!QwV7 z_)Wd`Lgy5#dxtvfxo=luj24tK%%%P2HV@*QA{)2)a?m)*tCiCRpm;JYKE6p$j?I=Z}8Wb}FlpB;WnBYB+^ z@LvYvwz1hgKG-pTgY$$H}2;vLFAaqJE#sV(;bE;EsE z$$``fY>kLCDqJzv6<^q(grjOFq0IlPL8YdLw|H<9Nd1o8E*iJ5PhbSVABex2fK zRM!Om5_P#fd85RvWGBJ6nm5|z!MYT~D7)K4VQ|h@Fa*fgQNf-;m2IgPE>EBAHUNLI zYY|d^aj-p#a5SomQs$Pm{?QnWVZ^nDRhCKtQE!DT>L>D>n9*cwce43Mi#^(9cjhNW zuJr71<@5qgon1oj9X-G|vOl7;d?@F+_hga|RHE0DNqbc^A)1~uO)q@msV03~x4ASM z^0FK5HUpd+k&VegMM0&u$O%kN&(Wo=;p~8WDhs{AQnio;$fU<*3>H5DDkL~lNaqJ?C{Zdq$d)L z0SdCJI?;@h+Argh^6BgE)q~Gn2mdnScs&4I_5oP=Dm&~O`cF;Cd`=oqUHhyfpx5NM zR@OQI4|(en@0HrzLL?&WgnT6>t5=UszvO^AwgA~jS+ z5fBI>T}lK*dI`NF3IwD|3B5?~n9%!Op?J>!z2EQezW2U-3&~z<&8(Sc%{()EZ*aF1 zwas7o`cz`KI|cg2^QB7lxJ|Ws%I2D!Lv|{(?}^x%qAwnv^hmq;mWK%s^R?7vH1nB* zr404^EZ663%%Glr%QZiutu+5S&&Y7}_{j^^ z^DkJv&Nv{InhfT)?)+q+yJz>SaM3T}+s?P;>DI4CrSsLQ+CXCR4y{s-jzJ2 zknzf^;^Dnlcl*~qF`J@K96L!8eQ9_~0)f`%vy*J!TDh*Yrni~iDVniB{m4&f{?$Dp z7xDbHl~*&sOjV9~CyOd3oH*CHe!uCgcx9K&x#Qp69Anql$vgS6If! z>D}V>FCwOk0p`2Z4-7dRHlUlXkIZYD0~Jg0;23yGjqrMU9JQL{EnrF!|L&7Zq1V;5 z=XFF=q_y$+h)&P2a6Tq4BwVJY593M<{laY)W^5Sv_oag0L*%ER{sSwfS+`hH^I1?$ ziHb$7Lxu!T@7D3}7G5nlDFxc(^fcV#@^W&c9%i1(6;S7gJSisDznOp zSt*Mrw<_M_EVoooC0;0NH#15;k@4B)&XQ$--(^!${QdriWEWzC&92CDFgex@TD;9P zC=PROZa1q;4pu95?L}&Q`jE#LGx?cI;$D-HY!ygeKTk*$NYg5bj2JzP%X`l~y?xL2 zIBL`E?6LjSBUq~NQ(#ueR-Q(iWEk_K%L5{xq=Y-BYG1P76fdWGLnB;wm+Eg?o(9I- zpD6hn(^}3lS}Z%)-TrEn&c}-+-E3W1%J1}qcBkK@2WOJStE|`U3(@M2yJ|G@d6sYL=97LfGkViT z=B}gm;67zVmqSS6m+J)-{>f(fEb(n*yK$f2$Lm0;n2;RzbX4BwsTz#Rzc^iE->NeY zRey+OPWt%e@(O~g0&B(fnGv1G>a!9#TMCysB}v(Kws64eK?a3OF7wka%kk6DW&OMc zZ<_v&w$J1wI=(LxY%`ek(S3=u*k4`^z7!&Wf-tkQ{mOdP$2?g}NC3u_klGU$XTTFz zt8qPKZsv#lb>|=b%kQEfO3cSKm_m9VCxipLM$wykZ7G%Jlak7eE1rFqf@x<@;r7pF zm19qDLjQFoSfQ_@43wfv>2s~`-MpMBx$u5-JbN;&J>}NNN5g5}WE-9QZ&F!}hUgMM z9w&o!EXwL+2id#8Om~!g%pP>FJsDW!-Lal3GiO{aXB7&BWq-b!%1|!%$_Hm2PR?P! zsm?bHb>9p9fmH2Fy07=1Dip`1#J$;b_58im3ke&D@3ehUE6LCPf_$K@BVCl$%PAZfO9c3 zpAnE&pZ1m!;{7&kWPE$5A-jr2H^jAdo2Z!* zfUwO0l7<7f)PBzgFF8-$(aJc{a6iBIytZW~&f!-Ej>7xnWcS(|yNWpiOk2SdHfd<8 z^2Us^t19`)S!m7=s==EtO39!j$1i|A>`Z?-dY$8O&ZAGn?q4==juaG~WGiuO6;kSY z5|9%n{6U|#0O*5FRx#k<0 z!gFUr`a@$*JGwdMSb0Z{Vw?0Ii9c~OT6QWm=`v5Y|Ik*PY^S>v0`6#qWz^rJb$%Bk z)PH^@5~fxcciiT|uL=gC{OnnBb#mplk4D{`ET(qB+SYY(&v+clyoCK|}ZJLtj-v>V*c5-^~CuOQWV=iJJk(NMePDJYemN=l85Wo3a5 zwZTdbv|LT;H3{SV z`!?24V6mxf?`w*q7fRG`ssAd|IRy(KYjqfiPmOTURuYn!+mcngKbNCD^*7vc()EkZO&$3vxv*Xu zkO5lhJhD|t2)jIR{)!VUt|HbpV=c8u_4iHg#J}Gdw7>tJ8uZZ81w=imk;lBb^X{cp zCD>|P?j8FSx?Gy@7J8<5}ck7wd3p9iPb;(&ImLDpf;CZ0GJJWZ1tjDUmUF$ z^-CuJG*I+kbZ{|;TbX`olX#lk?*V!AMy7Zn1Le#r&(Jw|>O_E2AN4{nX!*XfSy+%@ z*}HQ$0iJ_Jz9>6UC5o9$q2so@|LP&TVRCl*x6E%|8Tq!wu~R1M(^^ZHxR=eNW^5jk zDv9SPiaeP51n)$iYbieNnUTL!%_VX#l@#*pg)@QL>S15IO;(eWS;BmB!i@~WzSGjW z10m|vnaeYd>58M z>n+cSVZF^jdXBt3^x5TH>haM}#&$&yUdwo-YIvK$yRWm)^hVxubcgq=1cq9lR#L=f zc`b#RGz_k`e-4OdD}Us2Vt~V(*ajvRW#;fK#f>NR^u0?%QpCobpY#lDxVMa0R;;t* zTM%4dc?AFU!~I3oUagU(in=WONA8ze5eyRNIBt?DPKSM@j|=Tl+=JjIzSSX2;^SE# zfB(XN!%;~Z!cbmgCI0iLz?9Y;-U~EsWZ?#HO@b(a-Bf!;zn zAoR7(<;@aYT9^5>-XrYeKpr!D_a5l|@1CEQ(u5Ed{467Za2k`7Y*~P8P26W-#AyD`+h8ZNu~} z9kZh%_LMn$Z+5ocwLZBOPBlIFseQA2#$kWdvf}A>R&4ENSXAU(rj5)g=#2<;uMM%C z>X-D@*;Y)dHf2Y4gbxLDMo3g=`Ze6X!kGoqYvKZdi9KT~m1Mcawcq0Q+tFgBl>w{Mh#_Qr7-OoYV#AQ?8t_R2hLv+^Oq$1&l6e7{n{F6wKH*5D?Tu{h+mro|i*KZx8@xV|cE75#s~W5t2{k#6yDEP1o_HJw{q^#_O0yEB^V9sjI+1bs(@4Cr(%y8-G(# zQ!CxrKo;iZjcI6V4iE@Ax>{N`wzjra<~q-8eqXNh5fr3k$e|{sD34~Dr>0O>roQdT zlP8I-J*Q;8eEHHpHueT|lGxEXZd;lGhJMhU0gNJWnYZM}M6al$&BBB9@eO@zBxnT4 z3p?s}I!FF+cHxJ+b5cUw?4;7|rqhg2z22zbcl@07CHn~l)E&K0!*bkPy&je`|9i8P zD%o*fs;LWHxE<@2G!A|eAdeslvXJSUUB2Mky+ z`M$N|J$Rm(PSa8Q)H#MbLJw_2TaD1AsX-cSx3P`OvaQzg;2~-zEwL+i{4az4Ek_v{ znJSmgo8xT@Q;6(1OQ$|nM3Klhn`|Hs2^kR;iS1Faq>cKsj#lR1$j_!0yH`k0(TzSf zZ#_#98Ypwlp4WM2)BakkPj7eksj#!+7Y4Lamoahnz4MO}+>%=MpGGK0JU5AbZ_nSa z)W|u`yIKly-3f0A?XUY?Uw3uR(rJ|9gf^EE+%x8+q15zIAI&2Yt)oOLr=tzc{ z)|k-fk6bZzL|8*LJs4QaHD_B(@>DbWhM0*z&Eq;!iZJG8DZ`n*l5#P|j<3){p0g%3bi)f3>i<80k-hb34)mk< z(f_h-KwoO;a~GFVPH}N@72Ao%i+NX$#{ZA5>+1<&_rJfTSjqMpcfgdE_ASkc-V+1Q z6CjTL@>}=g*DptJ>iQ_$IJ(#(J4k3a`Yw8*tb+G*`yN=n8HHuGe;A%0+xw(3^W)X zdem{$jKytc{QrFYf1_Hq_vQx3vn}o2SZs{{`s-0FU6Bp!7BgMZC5}-otZ#eUy41*7kualwNk!)&_p1_P=R&hf&$9*N9k`dkczsW_G%2kNL;z>=DSa((X zTGIy6t@I^^t-nb~YM(m0pWpM}Yldr490=JXPXoc9m}TXPg~ix?o902RYEKF?1HtLN z1<96PuP;W3jA72GRsFoCHg<*4tqRe5#YBkgo%;P3BqU$gCDKjYZF*BoL<)?w4#!I- zN7*(@W#LblhlT)nxz3CzM(!#b<}5I>YY| zSxO3x79+P9;Drs5CUfQzXd9H!V-gZtS`(A8mL(1hzrtYHufwfK;D@S8BCSWgJ2iZK zZStzpXiYwibyd&EQ96vBU}>}YA+su6iyPT&q(-@E<56=lXjui00y5 zJ@o$9xq%w_g9c*G z#@V^CV(A@mfAXFKbT?w-oiDd3LFyZf!ujsuAXy5`j3fRi2?yBzIO0poZUHSl95QaJ zUz88@{?H~p$o6EBNTFwf=5g`?HjAvJu;O`YXbb%4K_589fEA^ z4N@$_#gU4BGStNSy4V=TD728~A-I#rB0wvzJY&phDpv6Ca*6002fLwMg^?fmShdYi z_z{d>G02Id5Z$bz$Uup2hRoz$Fic7TUePUvcw4*7M3?Z}r$xKDUrX)CN8ka`gx1y5 zYZ@?{n^^0UqyWkLmHsT7k`#-(4`%+l`QVR6WUCNawri5EgtCzjF~MqLwhv^hIoL40 zD{M&@nssfeN6~ILAV5W0CY{lKlsLzTf{`oEIH)9 z6Fg0j%0-g?Wo&_lvW8VZ9VW26s}K(zxChKxRyzdy7RtyGNCDH5Jr;!ZcGXsP zb#q*tk>W}6`*4CptE0yxMmNTpFj#Lgw?4@m-ge{6f6RpVAw!GK?rIjZHH25TPw??h zFtKn{WwNVT`5Ve=Rv{*)=Q$LkFHnU$$oe80f6cdWG~cM-vJ`juGQZQk8rxHzwh`-V z|AdhjQ zBTPC93hroDQD!(Z8~Q0hja5+G6f<%Pfs#2Ar&0BY=qvBn45NrTcI|VfVXer-7|55s z<$g8dk!oq8{3g>g!P4ym_o*+29*|t8ehL1NcWGLmZw8t(GBUroQ+)nPjLO5!k4(1f zGK}UH)l1ec%@iBW=*gYqBze9q@*B3R?&xA&sdfR#KT5%63~PC1da`q7B3*sFt}S(+ zC- zKg!9{&>;KE;cG+I5~|G9BQgr^+vocxF~7RmOS?`SBT1f@v^5+Rbhx*D{OW)Bea}5o z1?8_wjwHU)JStz!Eyrb|@j=*C%XST|MnQMOhD1>q=wU%gRN&NYsxzMDYF)k=n2{fY z4{C`$1rr-XurkA)^`g~-v2UEx%UTNHORXsJyy`VO$fPOm7YWI;3*p#_mTry>!E@ga zU0Pn>Mwa!xhgLw3>Q!hjzu$9lmjs-{vpd_`)5>qJs~HI_t{0TPukI2IjKMkZ^b~l) zDJ>n%uI$vt&!>OTgXCvKKG5j{E%G|!vwx8!H*J#Nue_yw?O%@b*^?a?Uk8^lW%5p+ z4@sj>^?AGSh9wWQ*Gi>VbUH0V%xD5JH7)Be7O7igj9#L)=7ze_9rfeNBC zq_YNL-&WL!eqSi)Ll@|^9a`j_PmA_>p*Jl_75el|92T9mW(VN3SD?mC{t{9lw3p6W znlv?Dt$2pHW!A@MDdTEAHV+O#$bVQR9M|{ZnPnk!#fCT`VWZnO6|$#wVt7>}K^6jOT#zI~#vrTUa5 zxqdCa0dBrTL%W0uG?!Q^YPFy3c#rMsB}3uFv3BXOAS`Z^4Arj6SGmiJW2x^`6|cxX zSGN$mrmkE30eg%jTl7B_@m#l(jk7pW@Pskn+Xq*uN){?^s2O4E*4sv+Ser++mTTf$I~9YtBzztf&ec{ z=Fedt6r*8N?Mz`wJ*{U(5op?K>BTK3&wu=t6I7&bHOLPEQCP4pRy8nyEqVS% zbVY+32vJ4;Wm`H`l+Vf`@hTzA`X+w?^ZJxQH0kd0G~`4T8_*f|wGrjF67G~RiLo6M z%SAt(5N>4Dhk{()J0V&wC~3J~o+{)+5qUjt zxXzmb<4=|ZiN$pt33Sn@!XUq2gNCuiE{GCr-au6m+KS*mS%!TXPNU^vYr0gUgaTHS z8*?8AKYF@!jKgnDMPje)8qrOZJ{G)4pyg07jk)>uPxgbWQ{{)=*)EW7p6IrJ;&Si4 zTK}YIxy>LE-r9|CujKG@Ryve(P~*iJ|#BF`m7dEmESkc;3&e z7Txq>dt@%6%u;LFjUl|YdYn6P@{wPb8*enjY%Vbr+N_=o-@aJ-*&1V45PpGxv zU<`Jwd#-$~TQ>6M#%8QQX4W;5>{yT!6l_$PHq+?y9Jo~v$VG}~@9Y{0J;OT_%-_US zyW9w8T#D^EL6RNwEQV#hzue%!MRH(tkR-IT14t<%YOHgY%-^t9_s}FN-D7sh8FC;Y znKj34ylNSkl{?IX?5ikerN&q^;LqetaJNGkG3U+rMLhEbNM70t&u)L6Tv&-efb@Wp z;ZvC5C%dnNHUfVI#U#s?h{<$qD2NE@D69m$Q{gC`7wH9o$c#tk*j2#_!})S#g&Zshh|xj`ss z_?SPe^)!VVWUTncKmpDJ$|%>ON}z#(9?_#19ZLCBHeH<3&u>CNJG%3H3o9LiwqDzZ zLVX-BDV8@&+`uH{5{#r~cCplzt9Al=k@`>}1;b9mCs?{8lGXfdfr5N4CD8MdLvj>ECuh>9^Vf#>5-yPD^^{4z6pYf~Y&@zG0g zmA+gPHTZ~OoBBC>{gIsJ!+C#tvKKT^FPXyh#%?5h&KmZ?{_v9Fd-qbJf_7YixiWeB zo@w(aGM~U z&&>uzS5X@&ZDu%)bdAzdzG$%EhulW;$CD1Du0LfR*v zoJ+N1IYv5NcVd>_O;TlXNz3y?OWyecUS(;{B!+vR3~jtms?ZgA-YBf}6U1rwRls~0 zrHN|wY;&NXq_3g6z|VZNmdihSxfnm}LwbusZ0b{Obp@@QjDLgN_dF+eWT`rMfCd3h z)CzcQp2DxQfPW5J=A|1IZD6OL5TBryK&V|r*i_rbmw(57B%u$ ziimb6yhKB-PpSY31{-#6y<%~z0zBxCD9KS1lQ;Pc4E>{G zt62aj1|ag0{Cv|_-HjJcczW5-Ur#FkAO!v{UtV{Hc)^SHGM!W;;m zZZI3JJsArChWGeKY?tvs5D_4H?Kf0GeA`5SXu3e>P+L~TR}$K2TCY3&Ld2BLVsI@_ zYlzMIA(dbBgrzxxyvJEDw$3pFjUPTKT_hkitBYkfFdTe#sgF0X*FyO$y{L6<=;U(gu z>-w75-@SvrW?L*x8$N~D4&xIw)<;!fFCOA%&l^wn>RGMj4?#t?_bkwBqDobiBU&|5 z1oy3KF($;BrQKS^gU~`aaX7g~n;w(>d`SBOehoAH-1JyGuiBWBvEJOQ4?8OPoW$?1 zwWBJ2HS&RytKIbijawH555f7%@Tm=rtcJRpZ#g6Ra-vjn%ITeypwhvgk^7SaY*n=| zo*vN^=G1?GsQziYVu0!4fl^P3O&$AhR^Iu4v1IlCmrOmiG?2Y1mYsx|<#x3!Rq^?} z#Aw@Q(EvA4cpJSTDedD?>oF;ie)1rZB4&5%uk7bASPd-1wo#E=6da0Wip1*N`jSN0 zuPX-?%7kmsk+U)6@1D;0<}1~MgN&Ufcw~FST}Rn?N%+iRO@`6F3b9Z;DeV3@;~k** zNoqNyDkoc3I3{Bp4dA**v;at3eXiN<8s#f0g0d%lV8q-+%QA-m<&VA_hh#Uggm;fC zreobG-wgI*ZlKihfnB;z^s5hs?Ioqdp5zQ31^!Dd(jG5(o29nq%!Vr5fy4oobdhTkH9iMpiFA? zJYjPFLj-j4Pl@l#uoksREb`gt%2~eVWFOBCfwid$%4k{*V~1g#qlIXcwDlPYXyM+~;Cxwfn+ zzw?1AJ#p64%cKrZ8p8%;*$XrD4>79PCnt z1)&fuh$MOqnJcId?T8E|bfd#q0soqvjw~NKYqa?wP!61`JWrfiV3(I;Xz8W(zMvJN z$A1=*m~QVZ_gut7Vf|x>_K}k#%cBtc4p@Ky35mZ_bUkHo3{*)>%7n#b4%2Cx6f@Bn zAe8ur>b@Io`rdHV0h~zDHI#eQK2&VGT<84|{10u$sElx37oUD#cEC#~N<7nX9aReq z6ue1@_)uh#ILO(|JIw>00QR!;4+CNT1n0Y#+DVEHQeYNEJ{&u}3r^rU#}cQyaKAj? zJMh?VY^rk6%;;<2;rm!O=hBK?ssD0)vgaspz-eA4Q0yBad~n`^>ay**ML8L?*U0Dz zMq-uyYVMA1cojW*ihFo|^kmQS=yJ>z@EEZ4qq6agCp+0IJu3em5Idtmv|qF`yJ@9GVGiE4^yiDwrG1CNL)-S{-flK)RM_?er%=4ZeE38FA0W^^5<1U#Q8kjaLc6g4vnzqB)is6%lu_*NO;#paGDNpj_)XKUnTkIbsy*DC}FK0QcBf;78Df zNzj7wsjqz>VRFvY&2a+0^^dK@4;OV^G>OI-;;*-S`^*7*0s&z+HG(eO>1_KJ1z!4_ z_F~C#lCRG142{P@;RvUv`>)y#H*}%g_Q!JK6^v9Zc=_M&+9KrG4R*gf9M=1yy@7hv z^85N){8Re}#+lO&Xgz})&DmP^8}G>jcnu!vB~70JC3r-Xz599e4x!+T!Vw(>=u|*N z7^@*p*MlvPo+1>)%JM}a;+ zm5GINWyLV_GytS=esG)cA1A0&<%N!T_t+KN0KBl?ddi0A`LS%P)s+RHdB<44Pz%oP zfjfuqu`QK0Z~GSx*vDvDt5;DDCAZhy;l0kVC-Y;pKu2O?dC5ocE&*6hEeF4hZ#=sc z9Q&6uz}|pAX9>|RKplpX++Fn=0a?fD{b(Qg`yGk_h2t?~>)REz2Rb3U8U9t72=Qd^ zU7o)4Yxp&V!My}`5ZJ1FGdoYo^6Grtt58W_!%Ge=Nv=tvi=vc>og)@!kN7_@)txFz zIe8j}_ATAct2hMh-SVN662r#=Dy^vjY5OpMLF zb2c2>Dj(9FA1Eypu!6yqmFX_zE{MC^s9MuW-y+KSZx6|-Csp7b6D>y_@0#dk06#Yj`ca8V^dA>MURs=ff!f&-V3>aEae9b+QI;M>fS7$R7`D>-_^rs5%o^--#n+O$m(EmwAM8p`_G!nZ1>A#x;@MF-jFi^TrIXnM;&7#NjqK>um)k$Wp zfxX?etaFmft+nf>0>~l2`hoC6!O{8K z{&9@(_$cKE;7+Y3fguvw4CA+bJ>dY4fZS)EwUeQ`)$}HUlwR|bp*j!QJe8}^kqwEd z+Z1Lrf4kvQhJEaBkJ$fvG{7_S=Nf#tF%KX!HiQl7(QZtr0Q63@+Xuhbn4?huh1^%+ zCHwvMNv20k$g!Z{95#cY^uUY3YxGfpE%7DqB8Xfp4C3!~$Y=pDEp%^^tI8{yW3R0? z|IydXrZyH%Ss2ySl0|FsVGIwC9|o&_d>wubT0-{v_sf$Kk8f~-76FMM!&<lePa5$h5`PY1Iuma*Bw(c4m$|Rli1KPx=Mr~AU!m?KW za3}z2OASDPw!|xV`W5r2NjyQ;of2d<%A=w<<70rt@;_93I;lecj1V1Ze$i74Nq*V| z|6EaHNt6V?RTo2zOc}0h&lJffo#kYClEu|#6mO0fs_XL)I%FpDhoK`r>CeGpbeCdi!+olSHw4UxSB;{?Gw~7m+;GA==D`6$i=l-ex`Om*J4XBixcLa^HD4h~ z#TIt8vB{rndRTY!?nA;ruojfZMd(P=s7&6DVH>D7kaL6OrFi;$845|e=^fDFxL?D# z2(O|HM~~$^4mEZQRHuPp=1+ikG1u;zBc>GxpEVxJ=np6_Tu_6jFamiO6bBry`g(-X zGBz1EE-%$nZ)nF5weHqs{PU-ie_2z+VWone9F^H1^@aQml$w1}WY>jA%HVsJwIM*V zG_6}V7Yk_}t?Ngzs)wXX{;*G1Xkir2-X!?EH{9HnK`SdvwdF9FIYf)_ypt=mX9 z6Okm|=`^-Pl{Il58aEiyEj$_g)M*#lXSx!33CT9Zh3l%@N@a5BHX@(5J@P!DgP};e zrMa}(%&Qnx1%DtoKSJ5~ReM>MP6#J5O69Qb00)6mTaX?^*>5tm(9{;xnuH6oY6din zl>wC}y4nqA+)C*zL^t(Wko~g-Eu!Hk+cy_J-5{E@sfA(G+8I>Ej^#*(QPO;!=O}nt z_Y?b{a&+n~9=l6v4c98mN;YTG(h4hg<~z@d^xq$_Ygg&aC>+!oKwz0V^&JP)2i(AI zO+Nd7b?<$PO&ej>l{>35VY*f+Qky>mH5Oflwo(f#Rx&ylcNZ2luDSFyq@uU$(do_7 zd)vnKlzu3A(-5UsDmi#kg^-JMhOUT}RL$~K=f1u{jRDuCrVTf?gTEZu2Jv<2>Ro!a zDl4|qYqp)zJ4t(XFQygw{}OJJr-i6_Y__NIWR<;mA^%qJy6pbnlO(Q*T0|}Of~KB8 zaXf4IiKxeLkc#w=ZO&$8RV*i78_!BOSiFz&xS!#+@ydiqWwG;wAi9T}tk$f9RKb-s zvvi?kCe3iiY*)wkP|!0QKQt>IluxTzr^XUOl5Q%#7PzQih4|$~r>dHQ3443^GqWXp zphsEBa+?y@(xTp_^wOU+5JW(0ewTr{%2~?&zaT!<#`3I^nba9fpqoX)T`f&>L!0U= zHxO&o2S1?f*{yaCmnN`Fw{g?pntFj@*2?#{XdcKX`&KReRdKV4UAlf1C1}Yjca}R8 ze21{vRq1}iqlNT)D=u}oXfCHA6$Do7EaHdbYA5u&Z_Rfq8E&@8*I)mf&kc=T%}Y`j z>9>)QGC0zCEpljUP*W1`t7Z{$R))T8xQOtBX9<5eL=ffpJD$wvH0Sy(vpq+_D@9+} zj|PZLzz?rxV{hCV+S{E0N^nhZ>gIn<10ild=UT^wg8_n??lrDlD<1nGzmd&UP(EC= z+H1ZvOLzF?*&phf`kmaD3VI7JKoB3_dC4Bi<{)o;tPLwMArQc3oMxd^g}~R)X%-GT zo4aQmt-GptJWMy0OLF>Nzzt&_3uG1k8(qcqN~yzz_uSr?M(<9`vtrD2b!^JE$FH?y z-8d>O@Y8H$rqe5vRxc>8pGHK_4J1sRdY>IT>9sZ~>Sbb3cDF-RGhR!e= zePoPxtz(xza|!);C&#OABe9DQ%(5z9!EJ9J&F5^}z*qPv5ZMT4@Tw^bfC3V9=etVA-Dc~-fwG}oczLB+P6>Y74|en?_Hnos}e3XM=D{^HLty+*UR z!<}3kaGacEJyNJmytz-llr2)g&x;JjXk8{Cee!6;#HHP}%1W3)qw#|0=ntN(#z4NK zt8(-ZH5USoC$#91V%vdvZAToL22Tq$dc9iS_vh10&+mLjM^-C-V=)76!Vl#yoy*zQ zI{LA0SUB+20JelCWm2pcoq-k+eKrtBYcJp}9CF|K9A5WSz8?HQs7o)HMdt9!Ck;!$ z4=P~KLY8SEbozF%!NNnu*AG9{9*x4*Fk6Gg*8!eZk*>P9&%@KLDkS+rk%fY~-i60lw zq^2>{$b)hmKnxAXAvec=ed(LY=Ip0u1+LQ6L&yz!A6+$E3~+>TT;9A2vD73oIe%o7 zf45+zck%MYFkWcvMx)7u<7PBr9@hnwUmAPm1X*Wp5Qgpuu0gB^x8t}AJVO?`)m z=98{cK?u#Vqk|BEe@XaAD)VqHmkcSMrHI?gh$SG7~<)-FdD#GFpL?lRVspgtHrUc+Znr8WodMDUh z!3}Lr;_`CZsnNlHz^>4{%MuPD8`WG=u<~DLts}N^+ps|ANAUQIBQ5BiITKAyL${UB z>+gF2$$;{d(TE9jn}Y|SpH8AZ+*iP>buUru!%+{Scq-oSBPlji(;yNXaMc4(=K+%( zz1Z?xnb!o*visd5*{WpvOPSIY*{YvRq~7UbEy!kDhQ*}c-=(*|Ba?~yREKH+C(TdZ zeRq_wqA8v)=u0)+Of)oeT}O9KC!r0>Cxo(6Dt6|rC%xaNa6?NHhTp4CQU*H7j_y3N z?cyQYSQrAtNmEtArF8#7qxJ(QA_y*`w zqhO`3jh-vM2HDs<$vCtwzmg!mnGis+!?CqGpSD{sy}{K9m@h^TO~Qxpvdhx_!y98< z(mhVG`B2*2Cl|K5mp$P<@XtViqp+~(0cxgHvBJF|u@QD?){T_X%@q-Y=m zFdH8WwSPui=`!3K@Eo0pvTpXM#BHGWmOO-|T5%&HQA=YhSsre1mi)b)$~{MBDJ>1W zk^6BgY(X1@NWc}V5+S@@sx3(e%T_B>qwNkm;wU;`$UMr#jP-lKj`F-U^RHgQe%-U6{ z#w=qNU6_qUr-1HdU?iaVr^Ri-wi85XgEM^A$c86Xb<(?|(wSCe_}YD^WiGM9P1CQ9 z>ke(=#*pPpF^ka>EyU#qgIkoMGV~lrSayH0q&k+*>IVx}@BXIPvw4xymmat_e$4~< zB5fYnlk`VdKACNoZSC!WlBcCZ6bOj)N{X5aYSF$+UsK<%W;<`aL$2JW4R2 z`6Egx+Ko`?u~mqq3#jg4M{Xgjmkes8UTCP>SkwS{)1>5cX&^8&yYdAs!wG;+rYpW` zt|O)52A5`E_|Hmg^|V+?I20{4i*tFf$lVGFurzt(1y$myFa)u$by9iZq>1~43B%TD z5fB59En0))c;&?(v!!TBpO+(1k;KbtRY_t1`i2%QFNl98n~OuVr`4{2bw#cd1p#NWVL7VLY9-< z=+@PonWH@`n|pdK?OYMa7?y|@yCmX~i~p6cQ9|=LjVdWydW16YxrDcN&0i_=%PR+- z_X1QG3J4K=AJkbAK$|r`a@`lb?pup)VY8wXQ^U>EcY#&8FRr;~iEYj0GI|V7p7i*A zGELX2m_u5b9kiaQ+y=W2WLi{OwtA|W|)CRZ*DJxT|&6Xs$d{^ zm3&G7R&V7H05#m#96bufE~#Reb$1tmD+f?yGpn|5O4hChZwN>-Y9*0{cm zS&gSFe?BdArkBc(viA7r4FPjZ(Y@%WRMf#DWxvjLW^Mn-QhpaMkx8raP?bgvKbfCN zv4+=bhs$i!+F#hJ)6N_2M!^g2YPk77y8`7e8uqMDyowRN9bJAt{Md2^>^!-~>Qntd zM2t2WNBH zzKE2kIFIuf_8>Qu*3&9C({`8>LgdI%tb-KRE`5CreJWci=*^V+Eve?m$bngduC09J zyt$#vP=)5s;mHsRUD|o+99NDR2WPk4q&Hs2^(CrQAf3=mvwx%pQjdrvB1AmfPEKo0 zj?%gRqGRZL6LFImH#9}fg{$`qJif4{l{ZX30u1MAAk44|uUomITeD_dw#zL}Sl0q5sih3)>KUH!zbN8ZRQ(p<<}$LDcv7Y zWy$i`nO*Eth`x8;#BXf=j>K9-dU&&qPwDIGNCwNAI*OfEiVH&&?L-BGEw$*=ke6IQ z1V+)wCW04Hv!uoTe#3RM;54y%u(HwOA!U8zA0*$HQjpeRI`4R%-h2=+bZSy0lN5y< z0{l`%m7`3HTF-x$iFxdFc?@(r@^Q-R`;sd4aWc78dKv7SA3Vqto(y(0Oi~^&X38J8 zs7$Vb^@Sky3)>*y1s1MCcgNIrFH=~L5wX9wJ9BktZ_A_Q`=SzX|KOdXvzA?$0K1sT z^M0nQiRN3}*GqElRtZ#Z#esLPw(==-p2 zv@mPqgjM)@z~Z&tk!z~7Lp*$J-Ju2T@uSWBxtE(G>igbm&1Ek$c8aB~U$1=U#{0nL z3=x2Gu*qq|5gZ6dRGB_stgddYjejX<`=e?ZRq$9=VC;L#vV~!3*k31p(qnfL>BNo~ z95LLR1&R=^ZVA$+F5GSE^IjjE7OVB6f%NUT`$(IYHqKD&4pR_*8;^XfFSGP%&{6zz z=<z{UR7blwbxL!-}ntH}`^alIx3I$H47&G$rDVV2vJ49k!2y42*Wof= zCb-dtmFV58^x%%3_Hc1K4tJN>!VI|$*bglfLKkq1wjYJ?P^7Vg!n+zI16x6#% zOj1=ArSn@G`U&#)-mu?c!;sc)MdJq!u{IrBzZ>Vd9Vgg9(QaN+I91TU?D*Tf4-V*Y;0KybD zD6D7qQx1d(Bh8nP(s8q>t4p*HnKG7<8y_V28=d`U^J1<%6l=HnbMjW344fdaSlt11 z&2HxA{Lt<^&b*|rqiZzWU6tn3w4rr-6&sg2rGrVkW|{b&AN{FpJ#-8g&$l&;Nj#y$DREW-nMp)=Lq<3(o|vi3H!90&OY%uAV!`<;aY zGxXgz#sys#Y|(w)Qrk0GyJqfnTmPE|-*Q81U-)YhZ}o#ON!>9WVFD-2Gq?VNTz=zU ztw?Ia?`7!L)g9`C#r)1olFSg`Z(OQCi!``r5tH`1iKhwVsOjq7zhMs5W6#6HvqBbT zey5vArU_VumeBI*9r!G`aklQjXJu$0L@iu}?jMMlK%1VY;p1_^yt?~|y|o3SanJ~I z3DozBFzDZILu+oxDYt{!&lkRG8vRCWhLEs(`p3wxiGLZ6XeMUF^(*80ateBbgw8B? z`^x!V&aAXH`;VLdbwA8p%5}BDO+^!_=`-|Msl!f0SngI>PXw8Xgx!ayp;Fw!$e*r$i5E6 z4`uTJ@ws&IS)<*T5cioRce6^2CC|2ZL^uCzo6@y`Mf{h8oZw&1Fd{lW|LtICpxs`6 z*ai{J4U`^z8aqXqO$Tz^Be{IpZ ztMq@d_g-O9Wn0@Qtxelv3kHG$!bSvDHo6B0^t6Am?t)Y86YEkghVE+4Q z7J-jV`5WDJAPEn3e}(ITOH|_bUGQNXO>v#NSFpX zb~O3NcWKF@2~);R+Qrf>q9VyKMBnMwLV?1ZWA*=ID(&VxpM@EdpH6)q24FjG%k2s;@>OAnYt0!)$xXa!SApWR=B*ZuD;{as{o-%cFXuy>Z3c9CoV%$MMB-g?}z zw91=E#@TiJXyAII9DV>zh6eJ|*|iCj4|fezllx&$^;rM4m^$R-blrQ>*hIS5)DqEa zK9=l8g(bqqd)}H{8sFX~n!a=oO&;p67NuKM+frNs58=M4$yeZs=6y^Tfy(GRIDZ`W zdBU8@?xxQKHX-tY*8SMm z{Te2oA^uczZOFzzqio**+Nb?L>>?I8(kMmTf!XgAAOt)j+e=4Ya%_?9c3s(w5y#Eu zdG7XB$L^NT!$;2F!gtrh&xXYK^Po1D2RnSwOYUXsYE%6B^|Dg};-}?L?U`|__}gT) zBbk@%J{>)|hce71WF1Z_;S0!7vVuVUYW z>`S-5B5hf#>?wTZE{lp@tlI1?Y=`mswIK>Yub*D@Ui4yNx5(U{hnq}#ZWW9M;3l8_;Fzm)ZNDOxMX)+SZBqqcnJmL+8skwcuanaEG z{{~C)46LT&4FH)Q)zqI(c^l-GaO!(SKc)aspx~3hG6~pTK~ddbvlYEtToiu9`tcoa zQg6M>>G8Q^;U#vX)eWv3)qUO%Sr9n0$E)IbSrByp7_!z2%#iDoM?nyw6_c_1S2IWEXIJRicE7wrtVAt#g`lq3(2s0kcm&3mGiQVRTJXz0|q_=U8SwDR&5Z-=9 zvYV?;pLxlBNhIWwqw)G@CReuZ(e=>LFLXsFQ5!$K*A|OIVe5wvJ$ggM@Gb{=Su?8<8591|T>J|TKMzJI=Sa&`L0iv(5hu1Aq@(bDszS^kxuu0=GK zk=3Q3W@i&Ws@Xol1RUPOYL*s=Ty=V0C+FR#pmBy%j}T7bPDj$3#&QasH;wIWl@mOG897{{o9}cd~dHQ?b zqHxe;M%Qe83-xa0Y+qf6$WVma$d`*hNgR)p-k*+cAkl{p@}Dk7hKawH=1y5LF5iyr zc$BgE%w+vp!;bk<)wE1u1*y+)MNn!5X?1@8iuzmyaq=SNHu`7FLfFz=VDRSL5jzs| zvGH_5$xUtfyprRQIEX4d=3E8@;qK_6gnyXq?tAU)d3A8DPa}|73V&^bwS2LiGIwm$ zsb)p0M(a*TxUVhRnvQmF-fT2>W`{XOu$hL=3}H=kIVVoY z>qche>7{L!IWA|0Qx1KK!-J+tryU_FRr$vF=nlDDJ=ni$aUP5}!R)hKM;)delFuZO zwqLTj-xgZ!qJ19~e%p>~;d!w#<3Dpv5wLoYb@_zgZ%grC-#;_s-(oucOdPW%v*@8t za`Rntb5e^iSEJ`*2fE^Hx#jc5%BeVO?&I+>V-3k``Ap~4W!FA*sBvS81TRasBRtm4 zh4Yu@?9iORq81Of19olb;g8gTxrnB0A2BI^&(d6pClxe3&VU6}=G`cTzpx>J z$!Wwr{Xw1ROS2f-2Y&H<(DzuL+@VX)itvA@<4;fr1lp+5vxacB-)(B-uUn!=xUrXu zik~=6) zb7o4wQSN>7QP8u?CX}Im(J(!Mg|WS-ogNK+EPoBn1t_g9F_CTUeWMy9-QjSb)ijpI z95k3i0tmCdRPIHx2C=}GenA5fo`^(ULruG<5Hbk=<0*|lpQ7Xjq>H*pm|D}`o{2;O z{;$6PiXv%eHB+yA*ijX)jWqHaFY?w};|1(;E0^TDzZbY+_#n^#p$*wf`i7j*%wJPO z1mozh6n)j> zO{5F4NE3ftY2onNKQ2Xfc=V6!xhxGW(%EpG^!_;>r%zwpgzo)unXN(>{J6f%AWhi2 z{(Iy9C0!Bb=hz{$rj5kbNJKl|%FVv=Y8~&D_|ZH-!UY8ILK1)?uSSyFm~*C47Xm^) z+-aS?Z_DIV6*Jd#9>`Al@x#VUCtZSG)?5Jv(ZF9W8yZaP*Tab*g$g;=W@KbBDJBKK zkWw{rue+mx=B5c_;KpY;ct%`;S|(H$$DJBtq5r%Hhh@*)mZk}l&@%Cl7!8EKeMci`qR&qgc5>o5QfCw$ zc}a3ADV0zT)_J%DzYcD1`eHi%oP%)%3XS8es~4w%5Xk%q0bQx4#p4roO}{gbIoy1K7YskK7?OhSSbb6g_4y zbsKrwDEFwWE9W8FImqItx;&Us&{*U^9pEAcKfS?m|M*E2vMi9FjB|=Lr~~$Xx+@y* zn%xZB2_Te*6NiwN3fHrGMiyV7ZbSmZ4NOS4STH%wsx^UxoRJY}PQ6X@<22Az-oA~r zgt6q@p|G&(vG)+IV=+O5a(7)Xl(Ts$n%IkbfdZH+s^s89hvyYz@5spQ^BM;F$4|I? z`Z|wv#O=_c$2a z#v&uU9%FcNIkf6;{ZP`y*WfwKd3rSCU?V1gGvG>k{!lut|B%W0=LT{~AaS|+I^eiz z?ZrpaQY<+5)<^yZ_t@qcnlP6tuVyLyQkY^>=L5*)97^8ZNERc*lA`i%a2G@F>o($| zH}R2fw#8&;dGdMC9YN;va8qYg>>!QYgx!n+oXQ62KeUo{RJ8HbC9b{ zCwVa+@V0N)}8Tf13`W-HPfBWn7sCJ z_T35JJKm80ZZYp_!xFcgq^kw2imnW#8bm0qUWO8E?YVf+i-?kC(vj^xbb14EYAy%@&{Tj?M;reGT>y+&(_H;3lP8)=}=1_Q*|_?Z!P_f&B=2A z`@pyWZ|b(hocEzX|awQ9DRsSj!lf+URZFG8}*ir&@&k9Xk{B6 ziA`Md2?H@hcs8+JVw$z^RLaLIe{wuMa#gt`&HDR`-?heO>JBP0(6F|J_($86kGO(& zkr8jpY~=p+*^)F%lXY&dwXj^H$8_R!nI2>_WQH*}U@iRQHC~!_rK?~;cg3rh9zxD%pYBs~N(~|V?LXkjlqEI)PQJ=27_~T+otf0k z|2)C|^Fd=A{<=C1Yx?UR=_Fnt?0#|2v$aFyaC?h~PD9xGEGsFLNnzQp1ig4AJs0s9UhFZw4(o4PVz_THN^ zXurzar}UbQmDl19#7VJZpPpz@#G#PGILE*lCoEWW4}k3#0c z7cZObO_J)!8Yp|d66Mz(uul{}XsL2lDNc%{`1I^(->87OkP70ui$05%>mj=NMU>_t z^VXOPOQOgc-@=!na;GBIWrLEyt+W`g3&Os4m`H6AtmKb7fA6aE?)F;0car`;!?|+2 z>pCy=Szm$C9Hukz1-xf08|xxw&Da^(Uw53j0@Ld_r_T$u+&yfpgrfUGH(o~@dp21c z3LRn+TqPHQxNEA~wL-a^sB`3Y(S3zGC$PipWLJe1ubKP1&;={x*{ScfbZhARXbyRQ zcS@Eg)(3bkcOsM{XJI!uYpNI-F8e{_w$EetB3354mVkfo~XMyn=Wi!nYnRb%A*aEmGO(y{Yn) z{%rc&`%li^+&pKm9U5$>FYixKc=pz_KIsfIVHpB)J-F%{iqB6#Z9=tQ@9q4sN9$2f zmIn6AOHM;ZVvSY!>P77@{6Vo&)t0vDYOg^5noLrqd4HL zjj<@y8yheUml^+XZFR`1J;bR)Mr;$d81}-%c%aM9IviD^^qTfmodTvLy8ouf_fH2f zjT2MbYop;NdXa@YLbCpK1I@&qLrPB=JV8IevIc};(@i*PMuZKM!0eT}mPO(r; zcai>ZS}|%Q>)riwtWUA0kHvcpUAqLxL2R*(PbXL_?3ug@seB&C)v|HLh+DVDEKKo_ zR@4^vcPbYPYRk6+S1PsmxfCx%B3*PLXxo|W|KqRkLr9Z6on$PostK4_m!Bi(cjMrL zj#)x(A$f?8`R08bH#UI?zI53x= zlrF?5_G<6hNgh)tk!ME<8WD(6lk^f{pB5w2OgaNFH^l(tUVL1y$wP<%uIp9llk^(Sxf=C zpBxtDSKJ0%RdT9zf6Q+luv$w6ADDy4rfVa5$7pC+Q7~his_3q8#!9&rlrO&MR-q0n zGHvuE+6ebQ9Vx(@rCqmU6c6Uk^af>J6^b6$&TY;0D(@?|j7p8qb`|BzT?QvyVTS9x3y=_DqP?5&= zeVsv2;-}MkIz#Lbt3t=QxuSFE#5UVX;uI=U#7za0bvDq`)#9pdTe2e#RbG#|7csES zM=$8m}1gr;gB& z6y{?J2S2Cc^Y7@~W4~_v`kdk9OcdNW_%02rfmeU^7Y_5GQcL`SwCl{o!Oo_Q{+gX{ zMloS*GDFsS+y?&r3v+SS1xtIa7v6X)U=Nujw8m0mp^Qgifw(#P$rr{)&6nYL;sQYZ zcQZ&3v)VP2lUtBekJkFRxxUcN%=&^=t3M1@7sf~^9c#===(1buWda?3xI0sZ0-x{X zw{YLBG1lyP-M;B5ymExgy__mg zLK@>dn<{SU=@#6XEz@BZu;1Yf_nL*H~dT?IURp!&eV4am*DnfJ2=748*0K7mf5q3&ev39)D9eq`OA(A4twpPXX-9-N7+2A?VW;r& z5s=BwlsR=<98Do8cx5t*M+w27%Z&ZKzsgnAmF1rfco=b7NsLj9{>Q%77;WM+i)_e{ z1lq%=Y;jrvCn!Wn@`dtM?vz{pw5OQc;zU|-+(H#5m2@()xPs%6WmR*a^*<@pWQFY7 zvvR<|RC!MxR@W$|VcoOL?NcyTtLScm08HMW^Q-J*%G*#BE|t>xj-fL2s^62n?X^@2 z+1+>Y4O6X`ZUF>Z2g_B(RB^nkd!N!Vqc3yvk!FL%4~*kepWYekRIuLN*~T2wW?&8W z&zT`3G$kH=fX)4`bkSQ>@|oO^DLpJS>5qd@yRfGTyPddkY+Hutx@RZ{VRKpR+1Cid z=&7=i2hm23Aq>q=4<4^Vy{ba}V|-qW{{(h@`G(U!KuRNY%Ve3gl5-8Y=&9z|Euh$+ zy}x}|NYi&I#H5aC0Uh{ew;B{=6F8zMVLQcmKUsEol0jTJxgp-JdbZ4Wb2f8AR$53%A7sctXhuF2op`5qv5%@?GUxSMN1cJ z^pA&BV9#J&NB?gK6yQ9rSwt@$m+ZBO5gUSl4JM@b;vj7L6DpU9vL%{@A2mdIc?Pzp z9so+jlWm1t8*H_HtjNWxm7C@-Gp!f}3?_+&+5fyC;cPnBeXcA7T`{QorHO!zx2wlC!%^7w=E{>Q zZ{GjWp2xxeZqL~Yp1#iC^Y1T1#$3a0?ruT(xQfOy4QmrIZcFp{YgtIzTY5%@?QlHA zqB2>r_|e9)`@y?1Oad<}?|ynbQ%fheIz;-G^&-9IE1)RGnrcrOy!V<>;i(^+e7f7Q zE*jor+J0%r&ocK4ZJ@Jmy+RBgPCeE_sn|`s@Z;S5>JmrBz8`Xz8IrnO%$hVoNS83e7=e)1?>Bvncj|l za##N6Qa^IF`1sHk%dtCtrXv+D_(P)yv8?+bH~f{J*_Z3NhgjpDIuLrY@#Hh7{(a|D zMqUBj@Am~*2pChw-L49=UW+s#eEwj}Ow>;y&R5$AYou{cmg0`dd18J!XIGUaE2d~Q zUQmaXS@(PTQeDmQ|K(>rUB`;J1wQ7tcP5-?IHvp0Hv*H71Y|0|pr)PexyO4ZK^m+n zcA&^q3jrMcVL#k}s})=+wCXSK9%4;=A4l_}Kc5~s=6Lmv4A)T`zX`|$AnzRcfnneq zmeLh{v^wvoY3~(l{OnAQbcus5j@WCR)U=)sKW_@YpUM}E*qNDq0i7W~EF%17R{!cE zhT{{H`0a~Pzy1(DGh*&zc;qKPQ~$$325cc5Ro&?&zj@8?$%h|M13r7vaBOp!B}i6d z>_~pE^{({K-)lUXVhGkk$ENeC1C3dbz7xGt3o2 zvs&;Hpa1<5KAX*TsRgonV|V}GK0M-|^Q*-igR|E1TF1PLKXUw~^UpoIWvUYV-t>q)X=7i& zt;v7={C-#dEC3*91wZ-ft!354@Ed!`3;tRCHe>MK&{@q-p2zW5%VwjTwH)Ltaz1kK zv#b96!}P*TA)y2DnR7I(SJi*be-3Yvkv=C*>}zV;lrR50zwoVL2uoKPbc+kaud(&! z8Ywooo>U-c|EIz~-%iQI!5@;SX*a-pr-@kix!e6}0!Mmenzg=BH$WU3zPw+b9^@K% zuX$aDUi>fre?FUCJ%6TN$&d3%#ad_luPLIOsnDPEmr9eOUmC4WUG@6-p~b^LdBX;iN`5A{{reZ372FCJXjr)}*gX6BNM)&7`&U*x_S&VkBa$?yevO6! zqmdI~=y)%0-$Z@)u5?`#GvFa}B2}!T$<#_)7^$yeAA8flt5(>UWT=g$l z;xS(Z+XRdx71h7o>46M`>+*LVB*PV0wlR&dz5h^sN<&T@gzK@AAHvpBQT-mi4@q04 zqWY7K8Vdfvbwv8VH`?on|Mt=U@AK&KIr}Qeal*bjr!_7OwVkjPE^{`K7T_6RA-$Ff zYNlvlA)T7u8+dZG%Ie_5TMWFCvM*X`rXYiK=*v2c6YiGhF;ID_W*9B)2gciB<_Ut^poX0Z3TGtsz8tiB%ID zp@$-Lrf-wGr53A>kNECxBRgjr!U9%&^=C@2{+8D~hRHI0; zx1s(!X?D`-dS!dA!jOY;$*6brCV>x^J_S{evdg?6EtJ^CmU!GHG2Ll$2Ah_KsPd}| zuq1u#=w$;E+pOyJckpFBYqy}#OCi_XzRDXc+xJF9$?_yf`^>a8DK=9fxXw*Rsuz?c zp1=1PBcZ^a14R)ol=+HP+y(5G2|`vqWJ#W$G`dKf zPhXxs^p&i%t)cf!N!E7Eqj6>{yPF5&fGV86yR7{#mAL3yP&RdUYg!JiA4+?A!P*HNork&(I?p?Q>qSNDym@yZ z&hw<4Pt}ODsAbog!u;*?a~?V}o%_7?Uw0&_8T+N3YLst3x%+@UIn|_(aH{4TOmGsq zg>w2!5d&3REnA7yY(iJTiq%mH^SL(J!ofw^HXi$|%;%Whs@6oQep_SH|9lgqR@BfT zZ%B|Gh))X8u#pIxi>%u9Zatag#Z>q<~=B>s7nKi2PwdTsjY8ECf=Z{Xw$+VZLc2tw) z)=os!ZCA~%nA+);=J&Qwa7_QTyTsKrcP{!?w0@;pAJdhve5y`qa?FV3^efi{WNdj< zO4_Zv=Jy2@*bQ%oGK=sUR5)9%ZcHbGW4fyG$a`vMEZ5rym{3=-3zRLdWwzPe>F;l7 zSeb>Qs=hG0ebWs8O=odf?!9bY-wO_TtMZ4Tl~z;n+z!B1_6*JJkQC9*e@j4w!HNiM zj3zGqRdM*JkG+n?mrLH8tJ=6TKncB5)9NvmH=T$eLx`>bbu7`PPapbK@&ReNbBlRl zJLk>_G9^s8)pS*T9 zrV|Brlez01aM{y}j5>@E!}Dz@I$)%BIp@0NF+=4KP(H(o(!WA|%Y>6U%$1!^;$b>_ zas!sT=E!V+Rh2}{u*XYhezhInUt^l+v-f_=xgFMBt@{F|L>$)2+?VcyGH#W~#DkMNkX}joecZGr3Ut8*2NMq?ZxiHZ!WW6x(uFl$Ekq6ZgWN^>M5 zZxV>hZ`%HdB%PM|x;07|0Yg+Kx^Up*&gWzca(qK#g44jedt~!)smevL`Zjig+T^8Z z;-?fkt1d;=lR<{|ouiy{wbPn!yS#50m^aI|dotWP^<3oOOPsOkX7i0O1Ani}x+J|` zTPl*s-aFi?*iN{|p6v5dwc-B5ZOL?@!X_f~{TXs8SeKXpQ$8wf)9^uV%Uuq4z6Lbn2KSu zB0FXUqvYjrbc<(q*2AlgUKnGVgKq6KGW|L86CGaNIQ_5^D0?vH4n$hdkVUL6HemQv zhNhiL_ES&WGennny_?NVzH_@w*kY}?PyODl!L(+Qx6NJB_-gaVc_YuNqt&R)EYd$> z7ba4iDH64^vZrcBDDHp;w1^`@!gO+5_R>rG%cC$XCCIQ4<}Vl)O~1rTXTlWARhYN# z30^U{bcmHxDJ*Yffox{?%6li++eF(V-0a{sE888vTJKseQ$J(SzYf(PG0LTy5A6Ta zS*XL@?x=VZohPO?Xoj&I9`s+~o_DZ3-JXuo{Uf_s)~C%scfnFc_+F^jy;P)pTx@RE zfUQ~Fo3meUN=Ayv7k>-;&H15Y->wkuxcntaT;sxm(t%;#XS579!`_pm`@fA@|HnI3 zhs*7V8xIL>*h%qK_MVp@`q=6uy{MMyV?r-oua@=D5DQu+qWV=oI*~7%0qGmx%TGnz56V#WF z&uI1g{LSaj=2e?_#caNqnah!48d+1{zIgF3EEh)Y8%7`)hK+nj<+H>(WlsulCa3q?vker-3!=IjgD8;tjx&3i8(du`$S zpKa4`K5#_r?m!%K!j-`AVhaZ{)*~{&N0jaMIi`Bd$8*~SDy3T7iKOLjB0gw{`_S^| z2h+ah;&3@3&)PZpEK+pIz_(ZC9Z885M(oaRUjm~X1yb2g*e)Jcb1l7JPlL-7Zfi?v z{N}o?x%z(8b4~y!{a|7ELm8P+ts@-%=ijA@G^|8fHaM&4()>hs9X1u+RAVV!Jfjyk zIU4b{w&g~{)c61mRcJ}${7tF<$3$B=zkRXxY48BDfC6(16KbFEK_cB|1Hci3c>eB0 zX{Adw+zdS4w?$le2F-zNY{z%SZD)J{MTb@S+)_{4et*HuNOB~%TCp0O=0EmZopE?QV|Nf|w{1>ytRxN+Tx_zy|NIrR(HnQUyN_mQl#Zw|eQ`_X>x ziVd&;CKpv=q&7;zqM0*dZcItLe;*raB`*|R{EyL*A|^yweEVl=m40;SH*y<{*Luc; zp(V{+u&Qr9qu*~$-dJ0e87J^ffB)AZR?n3)LOb8T)>`lG0tv2C=Jb%(6{nUgE7RNF zJ1-XMdE!0ADO#&|-ilL&R1};OE*D-|YCXtrQa!xTFkwt@>GR^W?lwr4>T(JEI$emN`aRjCd(YlJ$i+L4y7_+yI5 z;}JQVV^aE|6n{t4hy5-h6t%PzHroe6g_Y6o`9iYB2fE8rTBLXQd9?@nTf?R`BL-ha zTI}mSt#gQV9N)3NG(l_=@!XDdHTX<(dG3ES8KQ*-m2KM*(_tT6YIg(y=(_|5=dJi>)<7={|%hwqxw~L29v<&bXG;xJX9k`CSXY@*z@eW^AxWXu| zz2Ug~E*LKk+8vdg#0IlL*UcCGEb2YMVr?KQj~&X)U&xl6cExFyj;nCy$RdqBgR?%} z0YE`m#iC1B;hAS)KD6x?Q??n4OJX-cFp4)PFkI36=%}*;Sk;C>Q`gKytz-Gpg^Wdo8hq{6o{tuOPc(2? zhG~7h414Eq+ipEt^rc2q+ltk#w=Cvu#r@0_`V#cKuqf*S9CFs_bf^6dWzusJic$h02ozaMA`@8Ut z?bAH2;^W{yy{N#nGF@S6)TOdWkTG`~lBn;^*@M<)gep-uFuBLaSs<0&Xb}MGdYMyn zl@8mZ?^h~An75D28Mn+FFeJrbZzY4yN<`$55|;I>hwpyMR{Rp1%Ia$;ci=cNGYjnf z1s)0ZUMF}++^sKlF_zjSskyuoO-g&PAhU4xjkFwDpEZs&a@dTvzw=j^S|O$z4+dW2yRIdig)?3c$51}eMx9#a_qHf$-_ZH z1k8fw(hMTK=6AN(IozhNxhnRqqb7FoB{G-LACF4rAI(4LN-vdhsNVUzCf6#IF9~W~ zn|)*Snyi7oe)x@xI2+@kSrNoeBnqTEUC+E#NMhjAt?Oqqc#+Z?{?S9&SxH7UO2Cj2 zvrWdGa%rDGG;RN1s_Xxg>Z$_jAXhj4rxXV?YVpTGpzw<{{j*7p=bxCupB!hzQvS^N z$uYhFt=Hb&|4S{7?e)X|V)}JP#Kd>orPT3TvjsL)0bAvJlYh zAg;hB6qE!aJd3ZNpJ5^PgO8wOry6zC7_D{3BUJO_5{LrMKp3y!Td$r;nHo^gD)Rn+ z1-U{|^9=L$1PQXObO@7BnEzoGMpRKfBUY?>>97!FFx+!q#VRQQ2nt46{Ynomb?K#R zRfVCVLtiSBp+t?`Cb)9UGF#tk5yEgjL!i50dbEHt`#Wr!=528dG7uwlx#YGmJOjnW zo@3t0Wi93MW?Kcb1ncN4Et%K_feF8<+Fr#Z@4( zmi5_e_lU;^DDN4zL0}tdTfH$gma9S4nN2G`_VU%gslAwQWK^OCd8Lg+NzYpVVpX z!JScwEm8}$*-wR;g))W9VdaEBhOQ&TzeSQR=w&*dkPx;6(qVJ^(bs!e?m^K;*M7D( zu_CA-=7EcD6Y||?$=JP&|Iv}xAnxX~OBq#yM6;skueAY60jFGJ^!^$&i&Pu8(v`A^ zdG0mJ3qa0{g=$8PIqtrCFerA=ZE?ZK5^rdk2ikP-S2!qL7I< zgGA4u1|Tv!o*=sJhEc9%f8DHnniC`oc2_9{iZqPf#0GcI6rzo(h6GM`;wunL4K1LR zhDcjD7r8{9^?d=sCrMeJZ|U+V0g@BXFWHntS)|{_ei5bWG!#DC-{C{PwD@!#QftLF zVfNXqWTb!zy1b|*beMAL@`uH0icR;r)Ds^YG>X9}`2p#UFR+u2Le+DL6c-Id$KZwI zp_5&IgJN6L{=(a1166_4;=7A}bbmHgItZjp6S~)TNb(<)T-=M@i6vd2zYJ$dw4?t+#QZ;iEsi}$= zZ3a+@>W-s6#OO|Z%^4>CSu`J8UxO+H89}WmO1U!0w{js<%SFv^Vu!M*zMUd$Ue(Kd zPFJ}2Fx6@lQpeP_O4Chw?KTD0#*nBTGqRPf3g!z(kaBmSIM==S25slggg{a7g-g}6 z=3VX_%O8$Xl2^6ydH`}Jv}GIUN9W4)+ND=+;7U1&o;$>f57?AevkBWyD;rzHIvOb(l zIivkB(M6YVC(lAK1RmKTN<5m#PTb~+^Z!=|Q0ud0dIT9!l4Mg{hy)Qc z8!^XhOvPt=%Yaf1eUM5!)tI5_0?3wav$rdygc=c@eV1xp_F8~v)1Yaxj~aGZWuXT3 z?$aOHyX`>uQKdM1w3YH!8HDgpn#ndnFFT1lcxWiKtOa1J+(I?!gvilY88@B)b|3fE z)=M4f3+hA{`U3`xCH0fuD4~lUYA*bpvvAF zz0ugGcvjvdo}p;U)UCIdKD2Ew4}B4JgrbtYL$j)m7?W+`zql=P4?Qt&`{S=SQrYEC zytc4KyeXz}G)+LgMQAjD0Zx|O*#-_Z?9l$EqD5G5Aq$}2u>i*YMwyn4pS=8);qjti zIy&j^m$w!Vo;sitgAEo^k#ezYn_v`=uqeLh9jnTg7A(R0FT0X9QDZ}emi)E^i4xJK z=p8+IT9LoHP?=5;&rMXGR!`I;N-RL>{3oa(PDv$NcG=-db_MwxD>Df;zOQt9q|yRC zHHV1wYhE`ig*UfytzfuSzf;qOI{6UG`p1Q0Y*(LYnA}12o3*W4AEG|UAcTgcYG8PH z;tNZ(45$`*d`Ka0A53p&>HV#&J=j2Q&Jw%v7NR(1uK6>Pf4r%*NX?myNoAwyQ(!XE zt~9C@4z@_WxYqIo>Y}aN1euhr}tRjMh>dDieiSzVWG^pG#3;aHYi zSHDM->qze~dy_qbs!77IN&HY*0vvavgCsWMJ#KrzF$t*duAV2x)A|Z>;{?{qdPYO{ zwG<25+^UuAnZ`dVLsNr9L*{FCckr&w5STNK7uC6x>?;Fi4Nw2bFU_V8Adb7}NY&$K zHxk5)`iw`yLwH>__a&9;rPSwW<|%8BG!WZS0q@SNN)I18u4PKD+%MbjgI;z- zhj{07)9=|H0C#k)b8SkEa$aNd!K<_j1GwJsx*e;Vof(L5?o%VD8o_yUn3fjlTFgOdG@6h(U`qMIY}i z3h_OFK6Opjw`|y6%^0_{IF$txInsV8XhxXgqpQc_ePsB7-zs%=Aon|`HH=knP7hHh zghA7VWn@8i0YZk2qACKB+8OrysYn*e*Bx(Y6h4t&7?xvRi*qa90_>&b#QuQvXHlHd zn7vo}Fr>JUr*8M|3rGyD3S1Dg%)Jjuhl$+Qr1OWHwvfHP`}(|WNh;gy=Fw{{J^kH! zhgkn-`|g~|YCe+|_q)Nd!Woi!U}X#(asv&=ItiCx-Ad{>*B~8_Iza(-fDlcDq?Rmwb1&dSqFUt-lSTlh#C+ zuU~al5MXBV_p3%eONJ%Vep?Ss$#3#u9`U)|Y@( zsdUAi%S_OutQ``3|8bCpDyKyLcb$(}Oy&6zBG)>1PHRuS*UR{rA`v{CBDA?nFvzuy zPE!gG^Kw4pu%?soQDaK=7${OMo80p<$2{UMKM% z$8~dvhgwZ;h^2xXLTjWm*12`O$raYW6G7DQ_N{X3wjvF#9L7G}!&o6A>{|xoX9v0U zBQ%Z=p^BTh%X|yVZ=9y0*Esq~Mph&uFLH9cK12)Eymehud#rP*{S=!MxF-EZO>YUT ztVpkoOhTCaW};hUR=Inw00^QhO%OAPwhYxZDm#9~)1LTT>v$wtdvDZ^VrJ0* zn}%GGEGOwRTpNo}nQ_>5AJyCJfFVT|)GJA{Rl)O%vC6~?6AcpP{q+`uKKRt-HG~#_V!1nz*-f^V>uv=55FPHU9?iTn6D?_}C zHM`X)gEai&!Zm_QPj1>t0Q-gxa&hlP=#8|-I^U-Pc+Nv7M%^q!4tW?N#@%|G`8sD0 zdPH`{ESpuz(@LDt@q0w-)k8T^@etumh^$lU5ytl)%=RaOM^u6K?t63@Ib?=NhFC($ z_w`Byj1_eOt}t|4I4ncIP0&VcI2; ziRj849M}J362KQ}h!0L5jDco<%|~M{K3cc`0i5&SC#NnxNKCh;K>|-Vwo_o2H^$Y) zTR3m6FQJ1TA{yg(9}u~axfc()$G-lJd5}A$IiV7DjKP}f%~|1~+m1IR!1W}yJ#)^x ziPl@Bk>V`ZMLUKYLSBfP$gf{eMBR-6Rm*_$U4>lnrT z|2nBzsn)ohXUuBA+?K@ts>+lbS=(Y*QUv!@>9oo>NLegtJM_)y@|<*{TU#0jmq#r( zB4x%^Db}i?2hCaRSV)TQgzQY4758jIw~EJS46?ff&wT9)$8M&io7DJVL3+&0qymzN zOE~_%A6~6Cc(tz}BRASuI6I@_=AhgQ|LmC-Pd1FoNI_3C;Jg!byD`F+n$FsGSYQ+FKA@AeRD_+(d9G}M_d==z)ooYQ3BT1aEYiXrgMFkZ4CG;cRxv{|yH;+C*IGOpulC%q^ z4e13RhG$rfOL^)ba6Au&rqaKF=)8>vE%bW8VegtEBK`K8M5WPy)!Gju+>0d3{uR6a zZRZ@&#`3VJiE2&VH;L>gf(wKv>tmyD9Y!@d0iofE0}?ye(k%XILwzM~W}J$O`EBGR zm;^(qjI1x6JZq0l&bH=pxJzQq88Sy^nEqr)7;*9wun(+wW!|=FyXFEJXc>Jz?C0

    17U!I|N0s1-dl(l_w6 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 !e=R#6c`kL5a1VIx3B)W=NSvhEGB%pHn{9Y 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