From 47d0471b48909aa7ec1057431353ce9b96f29bb0 Mon Sep 17 00:00:00 2001 From: soraefir Date: Sun, 5 Jul 2026 15:01:38 +0200 Subject: [PATCH] Label sync and workflows --- .../android/bill_edit/EditBillActivity.kt | 27 +- .../android/bill_edit/EditBillScreen.kt | 22 +- .../android/bill_label/LabelBillsActivity.kt | 13 +- .../android/bill_label/LabelBillsScreen.kt | 8 +- .../currencies/ManageCurrenciesActivity.kt | 8 +- .../android/label/LabelManagementScreen.kt | 23 +- .../android/label/LabelManagementViewModel.kt | 42 +- .../cowspent/android/main/BillsListUtils.kt | 7 +- .../android/main/BillsListViewActivity.kt | 16 +- .../android/project/ProjectImportHelper.kt | 18 +- .../project/create/NewProjectActivity.kt | 1 - .../statistics/ProjectSankeyDiagram.kt | 12 +- .../statistics/ProjectStatisticsScreen.kt | 32 +- .../statistics/ProjectStatisticsTable.kt | 26 +- .../persistence/CowspentSQLiteOpenHelper.kt | 811 ++++++++++-------- .../persistence/CowspentServerSyncHelper.kt | 162 +++- .../net/helcel/cowspent/util/CategoryUtils.kt | 17 + .../helcel/cowspent/util/NextcloudClient.kt | 25 +- .../helcel/cowspent/util/ServerResponse.kt | 272 +++++- .../net/helcel/cowspent/util/SupportUtil.kt | 28 +- .../util/VersatileProjectSyncClient.kt | 432 +++++++++- app/src/main/res/values/strings.xml | 4 +- 22 files changed, 1443 insertions(+), 563 deletions(-) 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 index ef21ca1..e29c411 100644 --- 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 @@ -26,7 +26,6 @@ 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 net.helcel.cowspent.util.SupportUtil import java.text.ParseException import java.time.ZoneId @@ -52,23 +51,10 @@ class EditBillActivity : AppCompatActivity() { 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 == DBBill.CATEGORY_REIMBURSEMENT.toLong() }) - } - syncedCategories + hardcoded + db.getCategories(bill.projectId) } 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 } - } + db.getPaymentModes(bill.projectId) } EditBillScreen( @@ -168,7 +154,7 @@ class EditBillActivity : AppCompatActivity() { bill = DBBill( 0, 0, projectId, 0, 0.0, timeNowSeconds, "", DBBill.STATE_ADDED, DBBill.NON_REPEATED, - DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE.toLong(), "", DBBill.PAYMODE_ID_NONE.toLong() + DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE ) } else { val btd = db.getBill(billIdToDuplicate)!! @@ -195,6 +181,11 @@ class EditBillActivity : AppCompatActivity() { val project = db.getProject(bill.projectId) val currencies = db.getCurrencies(bill.projectId) + + if (project != null) { + db.ensureDefaultLabels(bill.projectId, project.type) + } + withContext(Dispatchers.Main) { viewModel.currencies = currencies viewModel.mainCurrencyName = project?.currencyName ?: "" @@ -399,6 +390,8 @@ class EditBillActivity : AppCompatActivity() { bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId ) newBill.billOwers = listOf(DBBillOwer(0, 0, memberId)) + newBill.categoryId = viewModel.categoryId + newBill.paymentModeId = viewModel.paymentModeId val newId = db.addBill(newBill) if (firstSavedId == 0L) firstSavedId = newId } 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 index 9f179d8..901469b 100644 --- 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 @@ -31,6 +31,7 @@ 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.util.Date import kotlin.math.abs @@ -466,9 +467,10 @@ fun BillAdditionalDetailsSection( modifier = Modifier.padding(bottom = 8.dp, top = 16.dp) ) + val context = androidx.compose.ui.platform.LocalContext.current var categoryExpanded by remember { mutableStateOf(false) } val selectedCategory = - categories.find { (if (it.id > 0) it.id else it.remoteId) == viewModel.categoryId } + categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId) EditableExposedDropdownMenu( value = selectedCategory?.name ?: "", @@ -495,9 +497,19 @@ fun BillAdditionalDetailsSection( Spacer(modifier = Modifier.width(12.dp)) Text(stringResource(R.string.category_none)) } - categories.forEach { category -> + + DropdownMenuItem(onClick = { + viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT + categoryExpanded = false + }) { + Text(text = "\uD83D\uDCB0", fontSize = 20.sp) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.category_reimbursement)) + } + + categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category -> DropdownMenuItem(onClick = { - viewModel.categoryId = if (category.id > 0) category.id else category.remoteId + viewModel.categoryId = category.id categoryExpanded = false }) { Text(text = category.icon, fontSize = 20.sp) @@ -512,7 +524,7 @@ fun BillAdditionalDetailsSection( var pmExpanded by remember { mutableStateOf(false) } val selectedPm = - paymentModes.find { (if (it.id > 0) it.id else it.remoteId) == viewModel.paymentModeId } + paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId) EditableExposedDropdownMenu( value = selectedPm?.name ?: "", @@ -541,7 +553,7 @@ fun BillAdditionalDetailsSection( } paymentModes.forEach { pm -> DropdownMenuItem(onClick = { - viewModel.paymentModeId = if (pm.id > 0) pm.id else pm.remoteId + viewModel.paymentModeId = pm.id pmExpanded = false }) { Text(text = pm.icon, fontSize = 20.sp) 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 index a0ad915..681fd8e 100644 --- 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 @@ -13,7 +13,6 @@ 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 @@ -42,14 +41,10 @@ class LabelBillsActivity : AppCompatActivity() { val billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED } val allCategorized = allBills.filter { it.categoryId != 0L && 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 == DBBill.CATEGORY_REIMBURSEMENT.toLong() }) - } - val categories = syncedCategories + hardcoded + db.ensureDefaultLabels(projectId, projectType) + + // Reload from DB to get real IDs + val categories = db.getCategories(projectId) Quadruple(members, billsToLabel, categories, allCategorized) } 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 index e2f841d..4e1889b 100644 --- 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 @@ -248,10 +248,10 @@ fun LabelBillsScreenPreview() { DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L) ) val cats = listOf( - DBCategory(-1, -1, 1, "Groceries", "๐Ÿ›’", ""), - DBCategory(-2, -2, 1, "Leisure", "๐Ÿฅณ", ""), - DBCategory(-3, -3, 1, "Rent", "๐Ÿ ", ""), - DBCategory(-4, -4, 1, "Bills", "๐Ÿ’ธ", "") + DBCategory(1L, 1L, 1L, "Groceries", "๐Ÿ›’", ""), + DBCategory(2L, 2L, 1L, "Leisure", "๐Ÿฅณ", ""), + DBCategory(3L, 3L, 1L, "Rent", "๐Ÿ ", ""), + DBCategory(4L, 4L, 1L, "Bills", "๐Ÿ’ธ", "") ) categories = cats categoriesMap = cats.associateBy { it.id } 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 index c47fd0d..a49f441 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt @@ -33,9 +33,11 @@ class ManageCurrenciesActivity : AppCompatActivity() { 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.action_currencies), - positiveText = getString(android.R.string.ok)) + viewModel.showDialog( + title = getString(R.string.dialog_sync_error_title), + message = getString(R.string.error_edit_remote_project_helper, message), + positiveText = getString(android.R.string.ok) + ) } } override fun onScheduled() {} diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt index 5568011..dcfedf8 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt @@ -28,6 +28,7 @@ import androidx.core.graphics.toColorInt import net.helcel.cowspent.R import net.helcel.cowspent.android.helper.ColorPicker import net.helcel.cowspent.android.helper.StatefulAlertDialog +import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBPaymentMode @@ -59,10 +60,10 @@ fun LabelManagementScreenContent( dialogState: net.helcel.cowspent.android.helper.DialogState?, onBack: () -> Unit, onAddCategory: (String, String, String) -> Unit, - onUpdateCategory: (Long, String, String, String) -> Unit, + onUpdateCategory: (DBCategory, String, String, String) -> Unit, onDeleteCategory: (Long) -> Unit, onAddPaymentMode: (String, String, String) -> Unit, - onUpdatePaymentMode: (Long, String, String, String) -> Unit, + onUpdatePaymentMode: (DBPaymentMode, String, String, String) -> Unit, onDeletePaymentMode: (Long) -> Unit, onDismissDialog: () -> Unit, onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit, @@ -123,7 +124,7 @@ fun LabelManagementScreenContent( if (selectedTab == 0) { CategoryList( - categories = categories, + categories = categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }, onEdit = { editingCategory = it showEditDialog = true @@ -162,7 +163,7 @@ fun LabelManagementScreenContent( if (showEditDialog) { if (selectedTab == 0) { EditLabelDialog( - title = if (editingCategory == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit), + title = if (editingCategory == null) stringResource(R.string.title_add_category) else stringResource(R.string.action_edit), initialName = editingCategory?.name ?: "", initialIcon = editingCategory?.icon ?: "", initialColor = editingCategory?.color ?: "#FF0000", @@ -171,14 +172,14 @@ fun LabelManagementScreenContent( if (editingCategory == null) { onAddCategory(name, icon, color) } else { - onUpdateCategory(editingCategory!!.id, name, icon, color) + onUpdateCategory(editingCategory!!, name, icon, color) } showEditDialog = false } ) } else { EditLabelDialog( - title = if (editingPaymentMode == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit), + title = if (editingPaymentMode == null) stringResource(R.string.title_add_payment_mode) else stringResource(R.string.action_edit), initialName = editingPaymentMode?.name ?: "", initialIcon = editingPaymentMode?.icon ?: "", initialColor = editingPaymentMode?.color ?: "#00FF00", @@ -187,7 +188,7 @@ fun LabelManagementScreenContent( if (editingPaymentMode == null) { onAddPaymentMode(name, icon, color) } else { - onUpdatePaymentMode(editingPaymentMode!!.id, name, icon, color) + onUpdatePaymentMode(editingPaymentMode!!, name, icon, color) } showEditDialog = false } @@ -353,8 +354,8 @@ fun LabelManagementCategoriesPreview() { MaterialTheme { LabelManagementScreenContent( categories = listOf( - DBCategory(1, 1, 1, "Groceries", "๐Ÿ›’", "#FF0000"), - DBCategory(2, 2, 1, "Rent", "๐Ÿ ", "#00FF00") + DBCategory(1L, 1L, 1L, "Groceries", "๐Ÿ›’", "#FF0000"), + DBCategory(2L, 2L, 1L, "Rent", "๐Ÿ ", "#00FF00") ), paymentModes = emptyList(), dialogState = null, @@ -379,8 +380,8 @@ fun LabelManagementPaymentModesPreview() { LabelManagementScreenContent( categories = emptyList(), paymentModes = listOf( - DBPaymentMode(1, 1, 1, "Cash", "๐Ÿ’ต", "#0000FF"), - DBPaymentMode(2, 2, 1, "Credit Card", "๐Ÿ’ณ", "#FFFF00") + DBPaymentMode(1L, 1L, 1L, "Cash", "๐Ÿ’ต", "#0000FF"), + DBPaymentMode(2L, 2L, 1L, "Credit Card", "๐Ÿ’ณ", "#FFFF00") ), dialogState = null, onBack = {}, diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt index 1421622..733eeed 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBPaymentMode +import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper -import net.helcel.cowspent.util.CategoryUtils class LabelManagementViewModel(application: Application) : AndroidViewModel(application) { private val db = CowspentSQLiteOpenHelper.getInstance(application) @@ -28,44 +28,30 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl fun loadLabels(projId: Long) { projectId = projId viewModelScope.launch { - val cats = withContext(Dispatchers.IO) { db.getCategories(projId) } - val pms = withContext(Dispatchers.IO) { db.getPaymentModes(projId) } - - if (cats.isEmpty()) { - val defaults = CategoryUtils.getDefaultCategories(getApplication(), projId) + val project = withContext(Dispatchers.IO) { db.getProject(projId) } + if (project != null) { withContext(Dispatchers.IO) { - defaults.forEach { db.addCategory(it) } + db.ensureDefaultLabels(projId, project.type) } - categories = withContext(Dispatchers.IO) { db.getCategories(projId) } - } else { - categories = cats - } - - if (pms.isEmpty()) { - val defaults = CategoryUtils.getDefaultPaymentModes(getApplication(), projId) - withContext(Dispatchers.IO) { - defaults.forEach { db.addPaymentMode(it) } - } - paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) } - } else { - paymentModes = pms } + categories = withContext(Dispatchers.IO) { db.getCategories(projId) } + paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) } } } fun addCategory(name: String, icon: String, color: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.addCategory(DBCategory(0, 0, projectId, name, icon, color)) + db.addCategoryAndSync(DBCategory(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED)) } loadLabels(projectId) } } - fun updateCategory(id: Long, name: String, icon: String, color: String) { + fun updateCategory(cat: DBCategory, name: String, icon: String, color: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.updateCategory(id, name, icon, color) + db.updateCategoryAndSync(cat, name, icon, color) } loadLabels(projectId) } @@ -74,7 +60,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl fun deleteCategory(id: Long) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.deleteCategory(id) + db.deleteCategoryAndSync(id) } loadLabels(projectId) } @@ -83,16 +69,16 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl fun addPaymentMode(name: String, icon: String, color: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.addPaymentMode(DBPaymentMode(0, 0, projectId, name, icon, color)) + db.addPaymentModeAndSync(DBPaymentMode(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED)) } loadLabels(projectId) } } - fun updatePaymentMode(id: Long, name: String, icon: String, color: String) { + fun updatePaymentMode(pm: DBPaymentMode, name: String, icon: String, color: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.updatePaymentMode(id, name, icon, color) + db.updatePaymentModeAndSync(pm, name, icon, color) } loadLabels(projectId) } @@ -101,7 +87,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl fun deletePaymentMode(id: Long) { viewModelScope.launch { withContext(Dispatchers.IO) { - db.deletePaymentMode(id) + db.deletePaymentModeAndSync(id) } loadLabels(projectId) } 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 index 7412547..05f6177 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt @@ -5,6 +5,7 @@ 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.CategoryUtils import net.helcel.cowspent.util.IRefreshBillsListCallback import java.text.SimpleDateFormat import java.util.* @@ -98,6 +99,10 @@ object BillsListUtils { context: Context ) { val timestamp = System.currentTimeMillis() / 1000 + val proj = db.getProject(projectId) + val categories = db.getCategories(projectId) + val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projectId, proj?.remoteId) + for (t in transactions) { val owerId = t.owerMemberId val receiverId = t.receiverMemberId @@ -106,7 +111,7 @@ object BillsListUtils { 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_NONE, reimbursementCategoryId, "", DBBill.PAYMODE_ID_NONE ) bill.billOwers += DBBillOwer(0, 0, receiverId) 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 index 18df8c1..c089426 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -21,9 +21,7 @@ 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.content.ContextCompat import androidx.core.content.edit import androidx.lifecycle.lifecycleScope @@ -50,6 +48,7 @@ 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.CategoryUtils import net.helcel.cowspent.util.CospendClientUtil import net.helcel.cowspent.util.ExportUtil import net.helcel.cowspent.util.ICallback @@ -555,11 +554,13 @@ class BillsListViewActivity : val members = db.getMembersOfProject(proj.id, null) val bills = db.getBillsOfProject(proj.id) + val categories = db.getCategories(proj.id) + val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId) val balances = HashMap() SupportUtil.getStats( members, bills, mutableMapOf(), balances, mutableMapOf(), mutableMapOf(), - -1000, -1000, null, null + -1000L, -1000L, reimbursementCategoryId, null, null ) Triple(proj, members, balances) } @@ -698,15 +699,6 @@ class BillsListViewActivity : } } - 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) 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 index a6b0a4d..e63f12e 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt @@ -149,16 +149,16 @@ object ProjectImportHelper { val memberNameToId = mutableMapOf() val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null)) - + val pmRemoteToLocal = mutableMapOf() - paymentModes.forEach { pm -> - val localId = db.addPaymentMode(DBPaymentMode(0, pm.remoteId, pid, pm.name, pm.icon, pm.color)) - pmRemoteToLocal[pm.remoteId] = localId + paymentModes.forEach { + val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color)) + pmRemoteToLocal[it.remoteId] = newId } val catRemoteToLocal = mutableMapOf() - categories.forEach { cat -> - val localId = db.addCategory(DBCategory(0, cat.remoteId, pid, cat.name, cat.icon, cat.color)) - catRemoteToLocal[cat.remoteId] = localId + categories.forEach { + val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color)) + catRemoteToLocal[it.remoteId] = newId } currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) } @@ -168,8 +168,8 @@ object ProjectImportHelper { bills.forEach { b -> val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L - val localCatId = catRemoteToLocal[b.categoryId] ?: b.categoryId - val localPmId = pmRemoteToLocal[b.paymentModeId] ?: b.paymentModeId + val localCatId = catRemoteToLocal[b.categoryId] ?: 0L + val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId)) billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower -> memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) } 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 index 0fc0bb2..b5be7bc 100644 --- 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 @@ -320,7 +320,6 @@ class NewProjectActivity : AppCompatActivity() { pid ) } - showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG) return pid } 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 index 951703d..58f4d9e 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt @@ -71,6 +71,7 @@ import net.helcel.cowspent.android.helper.formatShortValue import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.util.CategoryUtils import kotlin.math.abs import kotlin.math.roundToInt @@ -87,6 +88,8 @@ private object SankeyDimens { @Composable fun ProjectSankeyDiagram( projectName: String, + projectId: Long, + projectRemoteId: String?, allMembers: List, allBills: List, customCategories: List, @@ -96,8 +99,11 @@ fun ProjectSankeyDiagram( var selectedMemberId by remember { mutableLongStateOf(-1L) } var expanded by remember { mutableStateOf(false) } - val activeBills = remember(allBills) { - allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong() } + val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) { + CategoryUtils.getReimbursementCategoryId(customCategories, projectId, projectRemoteId) + } + val activeBills = remember(allBills, reimbursementCategoryId) { + allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != reimbursementCategoryId } } val membersMap = remember(allMembers) { allMembers.associateBy { it.id } } @@ -399,4 +405,4 @@ private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: F @Preview(showBackground = true, widthDp = 360, heightDp = 640) @Composable -fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} } +fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", 1L, "1", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} } 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 index d4fd63a..b231c7c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt @@ -84,6 +84,21 @@ fun ProjectStatisticsScreen( val statsData by produceState(null, proj.id) { value = withContext(Dispatchers.IO) { + val syncedCategories = db.getCategories(proj.id) + val defaultCategories = CategoryUtils.getDefaultCategories(context, proj.id) + + val toEnsure = if (proj.type == ProjectType.LOCAL) { + defaultCategories + } else { + listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT }) + } + + toEnsure.forEach { def -> + if (syncedCategories.none { it.remoteId == def.remoteId }) { + db.addCategory(def) + } + } + val members = db.getMembersOfProject(proj.id, null) val bills = db.getBillsOfProject(proj.id) val categories = db.getCategories(proj.id) @@ -94,19 +109,12 @@ fun ProjectStatisticsScreen( 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 == DBBill.CATEGORY_REIMBURSEMENT }) - } - (data.categories + hardcoded).distinctBy { it.remoteId } - } + val categories = data.categories + val categoryNoneLabel = stringResource(R.string.category_none) - val sankeyCategories = remember(proj.id, data.categories, defaultCategories, categoryNoneLabel) { + val sankeyCategories = remember(proj.id, data.categories, categoryNoneLabel) { val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "โŒ", "#9E9E9E") - (data.categories + defaultCategories + noneCategory).distinctBy { it.remoteId } + (data.categories + noneCategory).distinctBy { if (it.id == 0L) "none" else it.id.toString() } } when (selectedTab) { @@ -131,6 +139,8 @@ fun ProjectStatisticsScreen( 2 -> { ProjectSankeyDiagram( projectName = proj.name.ifEmpty { proj.remoteId }, + projectId = proj.id, + projectRemoteId = proj.remoteId, allMembers = data.members, allBills = data.bills, customCategories = sankeyCategories, 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 index ebb086f..71f21a1 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt @@ -44,8 +44,8 @@ fun ProjectStatisticsTable( 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 categoryId by remember { mutableLongStateOf(-1000L) } + var paymentModeId by remember { mutableLongStateOf(-1000L) } var dateMin by remember { mutableStateOf(null) } var dateMax by remember { mutableStateOf(null) } @@ -61,10 +61,10 @@ fun ProjectStatisticsTable( val shareStatsIntro = stringResource(R.string.msg_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 list = mutableListOf>() + list.add(Triple(-1000L, "๐Ÿ“‹", categoryAll)) + list.add(Triple(-100L, "๐Ÿงพ", categoryAllExceptReimbursement)) + list.add(Triple(0L, "โŒ", categoryNone)) val catsToUse = if (proj.type == ProjectType.LOCAL) { CategoryUtils.getDefaultCategories(context, proj.id) @@ -75,15 +75,15 @@ fun ProjectStatisticsTable( } catsToUse.forEach { - list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) + list.add(Triple(it.id, 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 list = mutableListOf>() + list.add(Triple(-1000L, "๐Ÿ’ณ", paymentModeAll)) + list.add(Triple(0L, "โŒ", paymentModeNone)) val pmsToUse = if (proj.type == ProjectType.LOCAL) { CategoryUtils.getDefaultPaymentModes(context, proj.id) @@ -94,7 +94,7 @@ fun ProjectStatisticsTable( } pmsToUse.forEach { - list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) + list.add(Triple(it.id, it.icon, it.name ?: "")) } list.distinctBy { it.first } } @@ -105,10 +105,12 @@ fun ProjectStatisticsTable( val membersPaid = HashMap() val membersSpent = HashMap() + val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(customCategories, proj.id, proj.remoteId) + SupportUtil.getStats( allMembers, allBills, membersNbBills, membersBalance, membersPaid, membersSpent, - categoryId, paymentModeId, dateMin, dateMax + categoryId, paymentModeId, reimbursementCategoryId, dateMin, dateMax ) var statsText = shareStatsIntro + "\n\n" diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt index e80da6a..ee1cb76 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -13,9 +13,9 @@ 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.CategoryUtils import net.helcel.cowspent.util.SecureStorage import net.helcel.cowspent.util.SupportUtil -import java.text.SimpleDateFormat import java.util.* /** @@ -156,10 +156,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : } @SuppressLint("Range") - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion < 1) { - } - } + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {} override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { recreateDatabase(db) @@ -202,311 +199,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : 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_ncUrl, accountProject.ncUrl) - values.put(key_name, accountProject.name) - values.put(key_archived, accountProject.archivedTs ?: 0L) - val id = db.insert(table_account_projects, null, values) - SecureStorage.savePasswordSync(context, "AccountProjectPassword_$id", accountProject.password) - return id - } - - 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 id = cursor.getLong(cursor.getColumnIndex(key_id)) - val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived)) - val password = SecureStorage.getPasswordSync(context, "AccountProjectPassword_$id") - return DBAccountProject( - id, - cursor.getString(cursor.getColumnIndex(key_remoteId)), - 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) - values.put(key_state, paymentMode.state) - 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)), - cursor.getInt(cursor.getColumnIndex(key_state)) - ) - } - - fun updatePaymentMode(id: Long, name: String?, icon: String?, color: String?, state: Int? = null) { - 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 (state != null) values.put(key_state, state) - 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) - values.put(key_state, category.state) - 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 getCategory(id: Long): DBCategory? { - val categories = getCategoriesCustom("$key_id = ?", arrayOf(id.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)), - cursor.getInt(cursor.getColumnIndex(key_state)) - ) - } - - fun updateCategory(id: Long, name: String?, icon: String?, color: String?, state: Int? = null) { - 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 (state != null) values.put(key_state, state) - 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 = ? AND $key_state != ?", - arrayOf(projectId.toString(), DBBill.STATE_DELETED.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())) - } + // --- Projects logic --- fun addProject(project: DBProject): Long { val db = writableDatabase @@ -571,16 +264,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : ) } - 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())) - SecureStorage.removePasswordSync(context, "ProjectPassword_$id") - } - fun updateProject( projId: Long, newName: String?, newEmail: String?, newPassword: String?, newLastPayerId: Long?, @@ -647,8 +330,19 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : } } + 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())) + SecureStorage.removePasswordSync(context, "ProjectPassword_$id") + } + + // --- Members logic --- + 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) @@ -705,7 +399,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : 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) } @@ -780,8 +473,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : db.delete(table_members, "$key_id = ?", arrayOf(id.toString())) } + // --- Bills logic --- + 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) @@ -866,7 +560,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : } } } - Log.v(TAG, "UPDATE BILL AND SYNC") val proj = getProject(bill.projectId) if (proj != null) syncIfRemote(proj) } @@ -887,29 +580,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : 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 - ) + fun getBill(id: Long): DBBill? { + val bills = getBillsCustom("$key_id = ?", arrayOf(id.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() @@ -1019,8 +694,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : db.delete(table_bills, "$key_id = ?", arrayOf(id.toString())) } + // --- Billowers logic --- + 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) @@ -1062,6 +738,443 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : db.delete(table_billowers, "$key_id = ?", arrayOf(id.toString())) } + // --- Categories logic --- + + 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) + values.put(key_state, category.state) + return db.insert(table_categories, null, values) + } + + fun addCategoryAndSync(cat: DBCategory) { + addCategory(cat) + val proj = getProject(cat.projectId) + if (proj != null) syncIfRemote(proj) + } + + 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 getCategory(id: Long): DBCategory? { + val categories = getCategoriesCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (categories.isEmpty()) null else categories[0] + } + + fun getCategories(projectId: Long, includeDeleted: Boolean = false): List { + val selection = if (includeDeleted) "$key_projectid = ?" else "$key_projectid = ? AND $key_state != ?" + val selectionArgs = if (includeDeleted) arrayOf(projectId.toString()) else arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()) + return getCategoriesCustom(selection, selectionArgs, null) + } + + fun getCategoriesOfProjectWithState(projId: Long, state: Int): List { + return getCategoriesCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.toString()), + null + ) + } + + @WorkerThread + private fun getCategoriesCustom(selection: String, selectionArgs: Array, orderBy: String?): List { + val db = readableDatabase + 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)), + cursor.getInt(cursor.getColumnIndex(key_state)) + ) + } + + fun updateCategory(id: Long, name: String?, icon: String?, color: String?, state: Int? = null, remoteId: Long? = null) { + 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 (state != null) values.put(key_state, state) + if (remoteId != null) values.put(key_remoteId, remoteId) + if (values.size() > 0) { + db.update(table_categories, values, "$key_id = ?", arrayOf(id.toString())) + } + } + + fun updateCategoryAndSync(cat: DBCategory, name: String?, icon: String?, color: String?, remoteId: Long? = null) { + val newState = if (cat.state == DBBill.STATE_ADDED) DBBill.STATE_ADDED else DBBill.STATE_EDITED + updateCategory(cat.id, name, icon, color, newState, remoteId) + val proj = getProject(cat.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun deleteCategoryAndSync(catId: Long) { + val cat = getCategory(catId) + if (cat != null) { + if (cat.state == DBBill.STATE_ADDED) { + deleteCategory(catId) + } else { + updateCategory(catId, null, null, null, DBBill.STATE_DELETED) + } + val proj = getProject(cat.projectId) + if (proj != null) syncIfRemote(proj) + } + } + + fun deleteCategory(id: Long) { + val db = writableDatabase + db.delete(table_categories, "$key_id = ?", arrayOf(id.toString())) + } + + // --- Payment Modes logic --- + + 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) + values.put(key_state, paymentMode.state) + return db.insert(table_payment_modes, null, values) + } + + fun addPaymentModeAndSync(pm: DBPaymentMode) { + addPaymentMode(pm) + val proj = getProject(pm.projectId) + if (proj != null) syncIfRemote(proj) + } + + 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 getPaymentMode(id: Long): DBPaymentMode? { + val pms = getPaymentModesCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (pms.isEmpty()) null else pms[0] + } + + fun getPaymentModes(projectId: Long, includeDeleted: Boolean = false): List { + val selection = if (includeDeleted) "$key_projectid = ?" else "$key_projectid = ? AND $key_state != ?" + val selectionArgs = if (includeDeleted) arrayOf(projectId.toString()) else arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()) + return getPaymentModesCustom(selection, selectionArgs, null) + } + + fun getPaymentModesOfProjectWithState(projId: Long, state: Int): List { + return getPaymentModesCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.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)), + cursor.getInt(cursor.getColumnIndex(key_state)) + ) + } + + fun updatePaymentMode(id: Long, name: String?, icon: String?, color: String?, state: Int? = null, remoteId: Long? = null) { + 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 (state != null) values.put(key_state, state) + if (remoteId != null) values.put(key_remoteId, remoteId) + if (values.size() > 0) { + db.update(table_payment_modes, values, "$key_id = ?", arrayOf(id.toString())) + } + } + + fun updatePaymentModeAndSync(pm: DBPaymentMode, name: String?, icon: String?, color: String?, remoteId: Long? = null) { + val newState = if (pm.state == DBBill.STATE_ADDED) DBBill.STATE_ADDED else DBBill.STATE_EDITED + updatePaymentMode(pm.id, name, icon, color, newState, remoteId) + val proj = getProject(pm.projectId) + if (proj != null) syncIfRemote(proj) + } + + fun deletePaymentModeAndSync(pmId: Long) { + val pm = getPaymentMode(pmId) + if (pm != null) { + if (pm.state == DBBill.STATE_ADDED) { + deletePaymentMode(pmId) + } else { + updatePaymentMode(pmId, null, null, null, DBBill.STATE_DELETED) + } + val proj = getProject(pm.projectId) + if (proj != null) syncIfRemote(proj) + } + } + + fun deletePaymentMode(id: Long) { + val db = writableDatabase + db.delete(table_payment_modes, "$key_id = ?", arrayOf(id.toString())) + } + + // --- Currencies logic --- + + 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 getCurrency(id: Long): DBCurrency? { + val currencies = getCurrenciesCustom("$key_id = ?", arrayOf(id.toString()), null) + return if (currencies.isEmpty()) null else currencies[0] + } + + 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 getCurrencies(projectId: Long): List { + return getCurrenciesCustom( + "$key_projectid = ? AND $key_state != ?", + arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()), + null + ) + } + + fun getCurrenciesOfProjectWithState(projId: Long, state: Int): List { + return getCurrenciesCustom( + "$key_projectid = ? AND $key_state = ?", + arrayOf(projId.toString(), state.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 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 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 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 deleteCurrency(id: Long) { + val db = writableDatabase + db.delete(table_currencies, "$key_id = ?", arrayOf(id.toString())) + } + + // --- Common Helpers --- + + 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 ensureDefaultLabels(projectId: Long, projectType: ProjectType) { + val allCats = getCategories(projectId, includeDeleted = true) + val defaultCats = CategoryUtils.getDefaultCategories(context, projectId) + val isLocal = projectType == ProjectType.LOCAL + + // Cleanup existing "fake" Reimbursement categories from DB to avoid duplicates + // and migrate bills to use the virtual constant directly. + allCats.filter { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT }.forEach { cat -> + val db = writableDatabase + val values = ContentValues() + values.put(key_category_id, DBBill.CATEGORY_REIMBURSEMENT) + db.update(table_bills, values, "$key_category_id = ?", arrayOf(cat.id.toString())) + db.delete(table_categories, "$key_id = ?", arrayOf(cat.id.toString())) + } + + // Only ensure other default categories for local projects. + // Reimbursement is now virtual and handled in UI/Sync. + val catsToEnsure = if (isLocal) defaultCats.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT } else emptyList() + + catsToEnsure.forEach { def -> + if (allCats.none { it.remoteId == def.remoteId }) { + if (!isLocal) def.state = DBBill.STATE_ADDED + addCategory(def) + } + } + + val allPms = getPaymentModes(projectId, includeDeleted = true) + val defaultPms = CategoryUtils.getDefaultPaymentModes(context, projectId) + val pmsToEnsure = if (isLocal) defaultPms else emptyList() + + pmsToEnsure.forEach { def -> + if (allPms.none { it.remoteId == def.remoteId }) { + if (!isLocal) def.state = DBBill.STATE_ADDED + addPaymentMode(def) + } + } + } + + // --- AccountProjects logic --- + + fun addAccountProject(accountProject: DBAccountProject): Long { + val db = writableDatabase + val values = ContentValues() + values.put(key_remoteId, accountProject.remoteId) + values.put(key_ncUrl, accountProject.ncUrl) + values.put(key_name, accountProject.name) + values.put(key_archived, accountProject.archivedTs ?: 0L) + val id = db.insert(table_account_projects, null, values) + SecureStorage.savePasswordSync(context, "AccountProjectPassword_$id", accountProject.password) + return id + } + + 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 id = cursor.getLong(cursor.getColumnIndex(key_id)) + val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived)) + val password = SecureStorage.getPasswordSync(context, "AccountProjectPassword_$id") + return DBAccountProject( + id, + cursor.getString(cursor.getColumnIndex(key_remoteId)), + 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) + } + @Suppress("ConstPropertyName") companion object { private val TAG = CowspentSQLiteOpenHelper::class.java.simpleName @@ -1100,16 +1213,16 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : 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" + const val key_category_id = "CATEGORYID" + 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" + const val key_icon = "ICON" + 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" diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index e4497b6..a750d01 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -29,9 +29,11 @@ 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.DBCategory import net.helcel.cowspent.model.DBProject import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.util.CospendClientUtil.LoginStatus +import net.helcel.cowspent.util.CategoryUtils import net.helcel.cowspent.util.ICallback import net.helcel.cowspent.util.IProjectCreationCallback import net.helcel.cowspent.util.NextcloudClient @@ -264,10 +266,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val members = dbHelper.getMembersOfProject(project.id, null) val memberIdToRemoteId = members.associate { it.id to it.remoteId } + + val categoriesToAdd = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_ADDED) + for (catToAdd in categoriesToAdd) { + try { + val categoriesResponse = client!!.getCategories(project) + val remoteCategories = categoriesResponse.getCategories(project.id) + val matchingRemote = remoteCategories.find { it.name == catToAdd.name } + + if (matchingRemote != null) { + dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, matchingRemote.remoteId) + catToAdd.remoteId = matchingRemote.remoteId + } else { + val createResponse = client!!.createRemoteCategory(project, catToAdd) + val newRemoteId = createResponse.remoteCategoryId + if (newRemoteId > 0) { + dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId) + catToAdd.remoteId = newRemoteId + } + } + } catch (e: Exception) { + Log.e(TAG, "CATEGORY SYNC FAILED for ${catToAdd.name}", e) + } + } + + val categoriesToEdit = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_EDITED) + for (catToEdit in categoriesToEdit) { + try { + client!!.editRemoteCategory(project, catToEdit) + dbHelper.updateCategory(catToEdit.id, null, null, null, DBBill.STATE_OK) + } catch (e: Exception) { + Log.e(TAG, "EDIT CATEGORY FAILED for ${catToEdit.name}, might not exist remotely", e) + // If it fails, we keep the state as EDITED so it tries again next time, + // or we could set it to OK if we think it's a permanent mismatch. + // For now just log it. + } + } + + val categoriesToDelete = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_DELETED) + for (catToDel in categoriesToDelete) { + try { + client!!.deleteRemoteCategory(project, catToDel.remoteId) + dbHelper.deleteCategory(catToDel.id) + } catch (e: NextcloudHttpRequestFailedException) { + if (e.statusCode == 404 || e.statusCode == 400) { + Log.d(TAG, "failed to delete category on remote project (code ${e.statusCode}) : delete it locally anyway") + dbHelper.deleteCategory(catToDel.id) + } else { + throw e + } + } catch (e: Exception) { + Log.e(TAG, "DELETE CATEGORY FAILED for ${catToDel.name}", e) + } + } + + val paymentModesToAdd = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_ADDED) + if (paymentModesToAdd.isNotEmpty()) { + try { + val pmsResponse = client!!.getPaymentModes(project) + val remotePms = pmsResponse.getPaymentModes(project.id) + val remotePmsNames = remotePms.map { it.name } + for (pmToAdd in paymentModesToAdd) { + val searchIndex = remotePmsNames.indexOf(pmToAdd.name) + if (searchIndex != -1) { + val remotePm = remotePms[searchIndex] + dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, remotePm.remoteId) + pmToAdd.remoteId = remotePm.remoteId + } else { + val createRemotePaymentModeResponse = client!!.createRemotePaymentMode(project, pmToAdd) + val newRemoteId = createRemotePaymentModeResponse.remotePaymentModeId + if (newRemoteId > 0) { + dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId) + pmToAdd.remoteId = newRemoteId + } + } + } + } catch (e: NextcloudHttpRequestFailedException) { + Log.e(TAG, "GET PAYMENT MODES FAILED : " + e.message) + } + } + + val paymentModesToEdit = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_EDITED) + for (pmToEdit in paymentModesToEdit) { + try { + client!!.editRemotePaymentMode(project, pmToEdit) + dbHelper.updatePaymentMode(pmToEdit.id, null, null, null, DBBill.STATE_OK) + } catch (e: Exception) { + Log.e(TAG, "EDIT PAYMENT MODE FAILED for ${pmToEdit.name}, might not exist remotely", e) + } + } + + val paymentModesToDelete = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_DELETED) + for (pmToDel in paymentModesToDelete) { + try { + client!!.deleteRemotePaymentMode(project, pmToDel.remoteId) + dbHelper.deletePaymentMode(pmToDel.id) + } catch (e: NextcloudHttpRequestFailedException) { + if (e.statusCode == 404 || e.statusCode == 400) { + Log.d(TAG, "failed to delete payment mode on remote project (code ${e.statusCode}) : delete it locally anyway") + dbHelper.deletePaymentMode(pmToDel.id) + } else { + throw e + } + } catch (_: Exception) { + } + } + val categories = dbHelper.getCategories(project.id) - val categoryIdToRemoteId = categories.associate { it.id to it.remoteId } + val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }.toMutableMap() + // Map hardcoded constants to themselves if not in DB + categoryIdToRemoteId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT + categories.filter { it.remoteId < 0 }.forEach { categoryIdToRemoteId[it.remoteId] = it.remoteId } + val paymentModes = dbHelper.getPaymentModes(project.id) - val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId } + val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }.toMutableMap() + paymentModes.filter { it.remoteId < 0 }.forEach { paymentModeIdToRemoteId[it.remoteId] = it.remoteId } val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED) for (bToDel in toDelete) { @@ -298,11 +411,15 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen for (bToEdit in toEdit) { try { val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId) - if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) { + val returnedRemoteId = editRemoteBillResponse.remoteBillId + if (returnedRemoteId == bToEdit.remoteId || (returnedRemoteId == 0L && !project.getRequestBaseUrl(true).contains("/ocs/v2.php"))) { dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK) - Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})") + Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)") + } else if (returnedRemoteId > 0) { + dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK) + Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)") } else { - Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})") + Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)") } } catch (_: Exception) { Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely") @@ -312,7 +429,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED) for (bToAdd in toAdd) { val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId) - val newRemoteId = createRemoteBillResponse.stringContent.toLong() + val newRemoteId = createRemoteBillResponse.remoteBillId if (newRemoteId > 0) { dbHelper.updateBill( bToAdd.id, newRemoteId, null, @@ -379,7 +496,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED) for (cToAdd in currencyToAdd) { val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd) - val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong() + val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId if (newRemoteId > 0) { dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK) } @@ -475,7 +592,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val localPaymentModes = dbHelper.getPaymentModes(project.id) for (localPaymentMode in localPaymentModes) { - if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) { + if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) { dbHelper.deletePaymentMode(localPaymentMode.id) Log.d(TAG, "Delete local pm : $localPaymentMode") } @@ -485,6 +602,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId } for (c in remoteCategories) { + if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue val localCategory = dbHelper.getCategory(c.remoteId, project.id) if (localCategory == null) { Log.d(TAG, "Add local category : $c") @@ -504,7 +622,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val localCategories = dbHelper.getCategories(project.id) for (localCategory in localCategories) { - if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) { + if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) { dbHelper.deleteCategory(localCategory.id) Log.d(TAG, "Delete local category : $localCategory") } @@ -532,7 +650,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val localCurrencies = dbHelper.getCurrencies(project.id) for (localCurrency in localCurrencies) { - if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) { + if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) { dbHelper.deleteCurrency(localCurrency.id) Log.d(TAG, "Delete local currency : $localCurrencies") } @@ -599,13 +717,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val dbMembers = dbHelper.getMembersOfProject(project.id, null) val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id } + val dbCategories = dbHelper.getCategories(project.id) + val categoriesRemoteIdToId = dbCategories.associate { it.remoteId to it.id }.toMutableMap() + // Map hardcoded constants to their local IDs if they exist in DB, else to themselves + categoriesRemoteIdToId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT + dbCategories.filter { it.remoteId < 0 }.forEach { categoriesRemoteIdToId[it.remoteId] = it.id } + + val dbPaymentModes = dbHelper.getPaymentModes(project.id) + val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap() + dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = 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) + billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) } else { - billsResponse.getBillsCospend(project.id, memberRemoteIdToId) + billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) } val remoteAllBillIds: List = if (isIHM) { remoteBills.map { it.remoteId } @@ -617,20 +745,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen val localBills = dbHelper.getBillsOfProject(project.id) val localBillsByRemoteId = localBills.associateBy { it.remoteId } - val syncedCategories = dbHelper.getCategories(project.id) - val catRemoteToLocal = syncedCategories.associate { it.remoteId to it.id } - val syncedPaymentModes = dbHelper.getPaymentModes(project.id) - val pmRemoteToLocal = syncedPaymentModes.associate { it.remoteId to it.id } - for (remoteBill in remoteBills) { - if (remoteBill.categoryId > 0) remoteBill.categoryId = catRemoteToLocal[remoteBill.categoryId] ?: 0 - if (remoteBill.paymentModeId > 0) remoteBill.paymentModeId = pmRemoteToLocal[remoteBill.paymentModeId] ?: 0 - 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)) { @@ -643,7 +762,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen ) nbPulledUpdatedBills++ updatedBillsDialogText += "โœ ${remoteBill.what}\n" - Log.d(TAG, "Update local bill : $remoteBill") } else { Log.d(TAG, "Nothing to do for bill : $localBill") } diff --git a/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt b/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt index 84a73b6..618d700 100644 --- a/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt +++ b/app/src/main/java/net/helcel/cowspent/util/CategoryUtils.kt @@ -46,4 +46,21 @@ object CategoryUtils { DBPaymentMode(DBBill.PAYMODE_ID_ONLINE_SERVICE, DBBill.PAYMODE_ID_ONLINE_SERVICE, projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc") ) } + + fun getReimbursementCategoryId(categories: List, projectId: Long, projectRemoteId: String? = null): Long { + return categories.find { + it.projectId == projectId && ( + it.remoteId == DBBill.CATEGORY_REIMBURSEMENT || + (projectRemoteId != null && it.remoteId.toString() == projectRemoteId) + ) + }?.id ?: DBBill.CATEGORY_REIMBURSEMENT + } + + fun getCategoryById(context: Context, id: Long, projectId: Long = 0): DBCategory? { + return getDefaultCategories(context, projectId).find { it.id == id } + } + + fun getPaymentModeById(context: Context, id: Long, projectId: Long = 0): DBPaymentMode? { + return getDefaultPaymentModes(context, projectId).find { it.id == id } + } } diff --git a/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt index 2e652d5..b8a1ded 100644 --- a/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt +++ b/app/src/main/java/net/helcel/cowspent/util/NextcloudClient.kt @@ -93,23 +93,30 @@ class NextcloudClient( params: Collection?, isOCSRequest: Boolean ): VersatileProjectSyncClient.ResponseData { + var finalTarget = target + if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) { + finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json" + } val result = StringBuilder() val headers: MutableMap> = HashMap() if (isOCSRequest) { val acceptHeader: MutableList = ArrayList() acceptHeader.add("application/json") headers["Accept"] = acceptHeader + val ocsHeader: MutableList = ArrayList() + ocsHeader.add("true") + headers["OCS-APIRequest"] = ocsHeader } val nextcloudRequest: NextcloudRequest = if (params == null) { NextcloudRequest.Builder() .setMethod(method) - .setUrl(target) + .setUrl(finalTarget) .setHeader(headers) .build() } else { NextcloudRequest.Builder() .setMethod(method) - .setUrl(target) + .setUrl(finalTarget) .setParameter(params) .setHeader(headers) .build() @@ -182,8 +189,12 @@ class NextcloudClient( target: String, method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean ): VersatileProjectSyncClient.ResponseData { + var finalTarget = target + if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) { + finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json" + } val result = StringBuilder() - val targetURL = url + target.replace("^/".toRegex(), "") + val targetURL = url + finalTarget.replace("^/".toRegex(), "") // Log.d(javaClass.simpleName, "method and target URL: $method $targetURL") val httpCon = SupportUtil.getHttpURLConnection(targetURL) httpCon.requestMethod = method @@ -194,7 +205,7 @@ class NextcloudClient( ) } httpCon.setRequestProperty("Connection", "Close") - httpCon.setRequestProperty("User-Agent", "cowspent-android/" + SupportUtil.getAppVersionName(context)) + httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context)) if (lastETag != null && METHOD_GET == method) { httpCon.setRequestProperty("If-None-Match", lastETag) } @@ -244,8 +255,12 @@ class NextcloudClient( target: String, method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean ): VersatileProjectSyncClient.ResponseData { + var finalTarget = target + if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) { + finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json" + } var strBase64: String - val targetURL = url + target.replace("^/".toRegex(), "") + val targetURL = url + finalTarget.replace("^/".toRegex(), "") val httpCon = SupportUtil.getHttpURLConnection( targetURL) httpCon.requestMethod = method if (needLogin) { diff --git a/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt index 3aeb6f5..4ae3e17 100644 --- a/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt +++ b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt @@ -68,7 +68,7 @@ open class ServerResponse( } val rawData = JSONObject(content) val data = rawData.getJSONObject("ocs") - return data.getString("data") + return data.get("data").toString() } class ProjectResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : @@ -137,6 +137,90 @@ open class ServerResponse( getResponseStringData().toLong() } + class CreateRemoteCategoryResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remoteCategoryId: Long + get() { + val dataStr = getResponseStringData() + return try { + dataStr.toLong() + } catch (_: NumberFormatException) { + val obj = JSONObject(dataStr) + obj.optLong("id", obj.optLong("remoteId", 0L)) + } + } + } + + class EditRemoteCategoryResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class DeleteRemoteCategoryResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class CreateRemotePaymentModeResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remotePaymentModeId: Long + get() { + val dataStr = getResponseStringData() + return try { + dataStr.toLong() + } catch (_: NumberFormatException) { + val obj = JSONObject(dataStr) + obj.optLong("id", obj.optLong("remoteId", 0L)) + } + } + } + + class EditRemotePaymentModeResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + + class DeleteRemotePaymentModeResponse( + response: VersatileProjectSyncClient.ResponseData, + isOcsResponse: Boolean + ) : ServerResponse(response, isOcsResponse) { + + @get:Throws(JSONException::class) + val stringContent: String + get() = getResponseStringData() + } + class CreateRemoteCurrencyResponse( response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean @@ -145,6 +229,18 @@ open class ServerResponse( @get:Throws(JSONException::class) val stringContent: String get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remoteCurrencyId: Long + get() { + val dataStr = getResponseStringData() + return try { + dataStr.toLong() + } catch (_: NumberFormatException) { + val obj = JSONObject(dataStr) + obj.optLong("id", obj.optLong("remoteId", 0L)) + } + } } class EditRemoteCurrencyResponse( @@ -196,6 +292,18 @@ open class ServerResponse( @get:Throws(JSONException::class) val stringContent: String get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remoteBillId: Long + get() { + val dataStr = getResponseStringData() + return try { + dataStr.toLong() + } catch (_: NumberFormatException) { + val obj = JSONObject(dataStr) + obj.optLong("id", obj.optLong("remoteId", 0L)) + } + } } class CreateRemoteBillResponse( @@ -206,6 +314,18 @@ open class ServerResponse( @get:Throws(JSONException::class) val stringContent: String get() = getResponseStringData() + + @get:Throws(JSONException::class) + val remoteBillId: Long + get() { + val dataStr = getResponseStringData() + return try { + dataStr.toLong() + } catch (_: NumberFormatException) { + val obj = JSONObject(dataStr) + obj.optLong("id", obj.optLong("remoteId", 0L)) + } + } } class DeleteRemoteBillResponse( @@ -242,13 +362,23 @@ open class ServerResponse( ServerResponse(response, isOcsResponse) { @Throws(JSONException::class) - fun getBillsCospend(projId: Long, memberRemoteIdToId: Map): List { - return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId) + fun getBillsCospend( + projId: Long, + memberRemoteIdToId: Map, + catRemoteIdToId: Map, + pmRemoteIdToId: Map + ): List { + return getBillsFromJSONObject(projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId) } @Throws(JSONException::class) - fun getBillsIHM(projId: Long, memberRemoteIdToId: Map): List { - return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId) + fun getBillsIHM( + projId: Long, + memberRemoteIdToId: Map, + catRemoteIdToId: Map, + pmRemoteIdToId: Map + ): List { + return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId) } @@ -270,6 +400,32 @@ open class ServerResponse( } } + class CategoriesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : + ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getCategories(projId: Long): List { + return if (isOcsResponse) { + getCategoriesFromJSON(getResponseObjectData(), projId) + } else { + getCategoriesFromJSONArray(getResponseArrayData(), projId) + } + } + } + + class PaymentModesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) : + ServerResponse(response, isOcsResponse) { + + @Throws(JSONException::class) + fun getPaymentModes(projId: Long): List { + return if (isOcsResponse) { + getPaymentModesFromJSON(getResponseObjectData(), projId) + } else { + getPaymentModesFromJSONArray(getResponseArrayData(), projId) + } + } + } + class AccountProjectsResponse( response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean @@ -366,6 +522,26 @@ open class ServerResponse( return members } + @Throws(JSONException::class) + protected fun getCategoriesFromJSONArray(jsonCats: JSONArray, projId: Long): List { + val categories: MutableList = ArrayList() + for (i in 0 until jsonCats.length()) { + val jsonCat = jsonCats.getJSONObject(i) + categories.add(getCategoryFromJSON(jsonCat, projId)) + } + return categories + } + + @Throws(JSONException::class) + protected fun getPaymentModesFromJSONArray(jsonPms: JSONArray, projId: Long): List { + val paymentModes: MutableList = ArrayList() + for (i in 0 until jsonPms.length()) { + val jsonPm = jsonPms.getJSONObject(i) + paymentModes.add(getPaymentModeFromJSON(jsonPm, projId)) + } + return paymentModes + } + @Throws(JSONException::class) protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List { val categories: MutableList = ArrayList() @@ -382,6 +558,27 @@ open class ServerResponse( return categories } + @Throws(JSONException::class) + protected fun getCategoryFromJSON(json: JSONObject, projId: Long): DBCategory { + var remoteId: Long = 0 + if (json.has("id") && !json.isNull("id")) { + remoteId = json.getLong("id") + } + 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 getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory { val remoteId = remoteIdStr.toLong() @@ -416,6 +613,27 @@ open class ServerResponse( return paymentModes } + @Throws(JSONException::class) + protected fun getPaymentModeFromJSON(json: JSONObject, projId: Long): DBPaymentMode { + var remoteId: Long = 0 + if (json.has("id") && !json.isNull("id")) { + remoteId = json.getLong("id") + } + 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 getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode { val remoteId = remoteIdStr.toLong() @@ -556,26 +774,30 @@ open class ServerResponse( protected fun getBillsFromJSONArray( json: JSONArray, projId: Long, - memberRemoteIdToId: Map + memberRemoteIdToId: Map, + catRemoteIdToId: Map, + pmRemoteIdToId: Map ): List { val bills: MutableList = ArrayList() for (i in 0 until json.length()) { val jsonBill = json.getJSONObject(i) - bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId)) + bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)) } return bills } @Throws(JSONException::class) protected fun getBillsFromJSONObject( - json: JSONObject, projId: Long, - memberRemoteIdToId: Map + memberRemoteIdToId: Map, + catRemoteIdToId: Map, + pmRemoteIdToId: Map ): List { val bills: List + val json = getResponseObjectData() if (json.has("bills") && !json.isNull("bills")) { val jsonBills = json.getJSONArray("bills") - bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId) + bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId) } else { bills = ArrayList() } @@ -586,7 +808,9 @@ open class ServerResponse( protected fun getBillFromJSON( json: JSONObject, projId: Long, - memberRemoteIdToId: Map + memberRemoteIdToId: Map, + catRemoteIdToId: Map, + pmRemoteIdToId: Map ): DBBill { var remoteId: Long = 0 var payerRemoteId: Long @@ -599,8 +823,8 @@ open class ServerResponse( var comment = "" var repeat = DBBill.NON_REPEATED var paymentMode = DBBill.PAYMODE_NONE - var paymentModeId = DBBill.PAYMODE_ID_NONE.toLong() - var categoryId = DBBill.CATEGORY_NONE.toLong() + var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE + var categoryRemoteId = DBBill.CATEGORY_NONE if (!json.isNull("id")) { remoteId = json.getLong("id") } @@ -639,20 +863,26 @@ open class ServerResponse( } if (json.has("paymentmode") && !json.isNull("paymentmode")) { paymentMode = json.getString("paymentmode") + } else if (json.has("paymentMode") && !json.isNull("paymentMode")) { + paymentMode = json.getString("paymentMode") } if (json.has("categoryid") && !json.isNull("categoryid")) { - categoryId = json.getLong("categoryid") - Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId") + categoryRemoteId = json.getLong("categoryid") + } else if (json.has("categoryId") && !json.isNull("categoryId")) { + categoryRemoteId = json.getLong("categoryId") } if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) { - paymentModeId = json.getLong("paymentmodeid") + paymentModeRemoteId = json.getLong("paymentmodeid") + } else if (json.has("paymentModeId") && !json.isNull("paymentModeId")) { + paymentModeRemoteId = json.getLong("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 && paymentModeId == DBBill.PAYMODE_ID_NONE.toLong()) { - Log.d("PaymentMode", "old: $paymentMode and new: 0") - paymentModeId = (DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE).toLong() + if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) { + paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE } + + val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L + val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L + val bill = DBBill( 0, remoteId, projId, payerId, amount, timestamp, what, DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId diff --git a/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt b/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt index 01e8dbf..c2b0cf9 100644 --- a/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt +++ b/app/src/main/java/net/helcel/cowspent/util/SupportUtil.kt @@ -87,14 +87,17 @@ object SupportUtil { membersBalance: MutableMap, membersPaid: MutableMap, membersSpent: MutableMap, - catId: Int, paymentModeId: Int, + catId: Long, paymentModeId: Long, dateMin: String?, dateMax: String? ): Int { + val proj = db.getProject(projId) + val categories = db.getCategories(projId) + val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projId, proj?.remoteId) return getStats( db.getMembersOfProject(projId, null), db.getBillsOfProject(projId), membersNbBills, membersBalance, membersPaid, membersSpent, - catId, paymentModeId, dateMin, dateMax + catId, paymentModeId, reimbursementCategoryId, dateMin, dateMax ) } @@ -106,7 +109,8 @@ object SupportUtil { membersBalance: MutableMap, membersPaid: MutableMap, membersSpent: MutableMap, - catId: Int, paymentModeId: Int, + catId: Long, paymentModeId: Long, + reimbursementCategoryId: Long, dateMin: String?, dateMax: String? ): Int { val nbBillsTotal = 0 @@ -124,9 +128,9 @@ object SupportUtil { for (b in dbBills) { // don't take deleted bills and respect category filter if (b.state != DBBill.STATE_DELETED && - ((catId == -1000 || catId == -100 || b.categoryId == catId.toLong()) && - (catId != -100 || b.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong()) && - (paymentModeId == -1000 || b.paymentModeId == paymentModeId.toLong())) && + ((catId == -1000L || catId == -100L || b.categoryId == catId) && + (catId != -100L || b.categoryId != reimbursementCategoryId) && + (paymentModeId == -1000L || b.paymentModeId == paymentModeId)) && (dateMin == null || b.date >= dateMin) && (dateMax == null || b.date <= dateMax) ) { @@ -285,18 +289,6 @@ object SupportUtil { 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 diff --git a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt index 27c93c2..2312552 100644 --- a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt +++ b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt @@ -11,8 +11,10 @@ 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.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 net.helcel.cowspent.model.ProjectType import org.json.JSONException @@ -82,13 +84,11 @@ class VersatileProjectSyncClient( project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId useOcsApiRequest = cospendVersionGT161 } else if (canAccessProjectWithSSO(project)) { - return if (cospendVersionGT161) { - target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId - 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) - } + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + return ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true) } else { useOcsApiRequest = cospendVersionGT161 target = if (cospendVersionGT161) @@ -313,10 +313,8 @@ class VersatileProjectSyncClient( payedFor = payedFor.replace(",$".toRegex(), "") paramValues.add(payedFor) paramValues.add(bill.paymentMode ?: "") - val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0 - val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0 - paramValues.add(remoteCatId.toString()) - paramValues.add(remotePmId.toString()) + paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString()) + paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString()) if (canAccessProjectWithNCLogin(project)) { username = this.username @@ -568,10 +566,8 @@ class VersatileProjectSyncClient( payedFor = payedFor.replace(",$".toRegex(), "") paramValues.add(payedFor) paramValues.add(bill.paymentMode ?: "") - val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0 - val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0 - paramValues.add(remoteCatId.toString()) - paramValues.add(remotePmId.toString()) + paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString()) + paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString()) if (canAccessProjectWithNCLogin(project)) { username = this.username @@ -650,7 +646,6 @@ class VersatileProjectSyncClient( } 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" @@ -714,7 +709,6 @@ class VersatileProjectSyncClient( 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" @@ -749,6 +743,92 @@ class VersatileProjectSyncClient( } } + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getCategories(project: DBProject): ServerResponse.CategoriesResponse { + 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 + "/categories" + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories" + return ServerResponse.CategoriesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161) + } 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) + "/categories" + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.CategoriesResponse( + requestServer( + target, METHOD_GET, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun getPaymentModes(project: DBProject): ServerResponse.PaymentModesResponse { + 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 + "/paymentmodes" + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes" + return ServerResponse.PaymentModesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161) + } 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) + "/paymentmodes" + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + return ServerResponse.PaymentModesResponse( + requestServer( + target, METHOD_GET, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) fun getMembers(project: DBProject): ServerResponse.MembersResponse { var target: String @@ -795,6 +875,305 @@ class VersatileProjectSyncClient( ) } + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.CreateRemoteCategoryResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(category.name ?: "") + paramKeys.add("icon") + paramValues.add(category.icon) + paramKeys.add("color") + paramValues.add(category.color) + + 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 + "/category" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories" + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category" + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories" + return ServerResponse.CreateRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories" + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + val response = requestServer( + target, METHOD_POST, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ) + return ServerResponse.CreateRemoteCategoryResponse(response, useOcsApiRequest) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.EditRemoteCategoryResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(category.name ?: "") + paramKeys.add("icon") + paramValues.add(category.icon) + paramKeys.add("color") + paramValues.add(category.color) + + 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 + "/category/" + category.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + category.remoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId + return ServerResponse.EditRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + category.remoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + category.remoteId + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + category.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.EditRemoteCategoryResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun deleteRemoteCategory(project: DBProject, categoryRemoteId: Long): ServerResponse.DeleteRemoteCategoryResponse { + 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 + "/category/" + categoryRemoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + categoryRemoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId + return ServerResponse.DeleteRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + categoryRemoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + categoryRemoteId + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + categoryRemoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.DeleteRemoteCategoryResponse( + requestServer( + target, METHOD_DELETE, null, null, + null, username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun createRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.CreateRemotePaymentModeResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(paymentMode.name ?: "") + paramKeys.add("icon") + paramValues.add(paymentMode.icon) + paramKeys.add("color") + paramValues.add(paymentMode.color) + + 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 + "/paymentmode" + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes" + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode" + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes" + return ServerResponse.CreateRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode" + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes" + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes" + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.CreateRemotePaymentModeResponse( + requestServer( + target, METHOD_POST, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun editRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.EditRemotePaymentModeResponse { + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + paramKeys.add("name") + paramValues.add(paymentMode.name ?: "") + paramKeys.add("icon") + paramValues.add(paymentMode.icon) + paramKeys.add("color") + paramValues.add(paymentMode.color) + + 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 + "/paymentmode/" + paymentMode.remoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentMode.remoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId + return ServerResponse.EditRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentMode.remoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentMode.remoteId + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.EditRemotePaymentModeResponse( + requestServer( + target, METHOD_PUT, paramKeys, paramValues, null, + username, password, bearerToken, useOcsApiRequest + ), useOcsApiRequest + ) + } + + @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) + fun deleteRemotePaymentMode(project: DBProject, paymentModeRemoteId: Long): ServerResponse.DeleteRemotePaymentModeResponse { + 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 + "/paymentmode/" + paymentModeRemoteId + else + project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId + useOcsApiRequest = cospendVersionGT161 + } else if (canAccessProjectWithSSO(project)) { + target = if (cospendVersionGT161) + "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentModeRemoteId + else + "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId + return ServerResponse.DeleteRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161) + } else { + useOcsApiRequest = cospendVersionGT161 + target = if (cospendVersionGT161) + project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentModeRemoteId + else + project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentModeRemoteId + } + } else { + target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId + username = project.remoteId + password = project.password + bearerToken = project.bearerToken + } + + return ServerResponse.DeleteRemotePaymentModeResponse( + requestServer( + target, METHOD_DELETE, 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() @@ -952,6 +1331,10 @@ class VersatileProjectSyncClient( nextcloudAPI: NextcloudAPI, target: String, method: String, paramKeys: List?, paramValues: List?, isOCSRequest: Boolean ): ResponseData { + var finalTarget = target + if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) { + finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json" + } val result = StringBuilder() var params: MutableList? = null if (paramKeys != null && paramValues != null) { @@ -965,17 +1348,20 @@ class VersatileProjectSyncClient( val acceptHeader: MutableList = ArrayList() acceptHeader.add("application/json") headers["Accept"] = acceptHeader + val ocsHeader: MutableList = ArrayList() + ocsHeader.add("true") + headers["OCS-APIRequest"] = ocsHeader } val nextcloudRequest: NextcloudRequest = if (params == null) { NextcloudRequest.Builder() .setMethod(method) - .setUrl(target) + .setUrl(finalTarget) .setHeader(headers) .build() } else { NextcloudRequest.Builder() .setMethod(method) - .setUrl(target) + .setUrl(finalTarget) .setParameter(params) .setHeader(headers) .build() @@ -1009,8 +1395,12 @@ class VersatileProjectSyncClient( lastETag: String?, username: String?, password: String?, bearerToken: String?, isOCSRequest: Boolean ): ResponseData { + var finalTarget = target + if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) { + finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json" + } val result = StringBuilder() - val httpCon = SupportUtil.getHttpURLConnection(target) + val httpCon = SupportUtil.getHttpURLConnection(finalTarget) httpCon.requestMethod = method if (bearerToken != null) { httpCon.setRequestProperty("Authorization", "Bearer $bearerToken") @@ -1021,7 +1411,7 @@ class VersatileProjectSyncClient( ) } httpCon.setRequestProperty("Connection", "Close") - httpCon.setRequestProperty("User-Agent", "Cowspent/" + SupportUtil.getAppVersionName(context)) + httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context)) if (lastETag != null && METHOD_GET == method) { httpCon.setRequestProperty("If-None-Match", lastETag) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81e9cf1..fbdadd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Settle Project Share Project Add Project + Add Category + Add Payment Mode Nextcloud Account Web link Cowspent link @@ -229,7 +231,7 @@ Settlement - Choose Currency + Choose Currency (%s) None All Currency settings saved.