5 Commits

Author SHA1 Message Date
sora 553e33d441 Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2026-07-12 14:47:56 +02:00
sora eec95cdcb0 Merge branch 'main' into renovate/configure 2026-07-12 14:47:47 +02:00
soraefir 106c4e27fc Fix sync and add beta features 2026-07-09 19:22:04 +02:00
soraefir 47d0471b48 Label sync and workflows 2026-07-05 15:02:36 +02:00
Renovate Bot c2f59fa1df Add renovate.json 2026-05-30 02:02:13 +00:00
29 changed files with 1488 additions and 582 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ android {
applicationId "net.helcel.cowspent" applicationId "net.helcel.cowspent"
minSdk = 26 minSdk = 26
targetSdk = 37 targetSdk = 37
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1" versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
} }
@@ -26,7 +26,6 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser import net.helcel.cowspent.util.BillParser
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.text.ParseException import java.text.ParseException
import java.time.ZoneId import java.time.ZoneId
@@ -52,23 +51,10 @@ class EditBillActivity : AppCompatActivity() {
ThemeUtils.CowspentTheme { ThemeUtils.CowspentTheme {
val categories = remember { val categories = remember {
val syncedCategories = db.getCategories(bill.projectId) 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
} }
val paymentModes = remember { val paymentModes = remember {
val syncedPaymentModes = db.getPaymentModes(bill.projectId) db.getPaymentModes(bill.projectId)
val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId)
if (projectType == ProjectType.LOCAL) {
syncedPaymentModes + defaultPaymentModes
} else {
syncedPaymentModes.ifEmpty { defaultPaymentModes }
}
} }
EditBillScreen( EditBillScreen(
@@ -168,7 +154,7 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill( bill = DBBill(
0, 0, projectId, 0, 0.0, timeNowSeconds, 0, 0, projectId, 0, 0.0, timeNowSeconds,
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED, "", 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 { } else {
val btd = db.getBill(billIdToDuplicate)!! val btd = db.getBill(billIdToDuplicate)!!
@@ -195,6 +181,11 @@ class EditBillActivity : AppCompatActivity() {
val project = db.getProject(bill.projectId) val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId) val currencies = db.getCurrencies(bill.projectId)
if (project != null) {
db.ensureDefaultLabels(bill.projectId, project.type)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
viewModel.currencies = currencies viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: "" viewModel.mainCurrencyName = project?.currencyName ?: ""
@@ -399,6 +390,8 @@ class EditBillActivity : AppCompatActivity() {
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
) )
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId)) newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
newBill.categoryId = viewModel.categoryId
newBill.paymentModeId = viewModel.paymentModeId
val newId = db.addBill(newBill) val newId = db.addBill(newBill)
if (firstSavedId == 0L) firstSavedId = newId if (firstSavedId == 0L) firstSavedId = newId
} }
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.* import net.helcel.cowspent.android.helper.*
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.util.Date import java.util.Date
import kotlin.math.abs import kotlin.math.abs
@@ -466,9 +467,10 @@ fun BillAdditionalDetailsSection(
modifier = Modifier.padding(bottom = 8.dp, top = 16.dp) modifier = Modifier.padding(bottom = 8.dp, top = 16.dp)
) )
val context = LocalContext.current
var categoryExpanded by remember { mutableStateOf(false) } var categoryExpanded by remember { mutableStateOf(false) }
val selectedCategory = 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( EditableExposedDropdownMenu(
value = selectedCategory?.name ?: "", value = selectedCategory?.name ?: "",
@@ -495,9 +497,19 @@ fun BillAdditionalDetailsSection(
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_none)) 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 = { DropdownMenuItem(onClick = {
viewModel.categoryId = if (category.id > 0) category.id else category.remoteId viewModel.categoryId = category.id
categoryExpanded = false categoryExpanded = false
}) { }) {
Text(text = category.icon, fontSize = 20.sp) Text(text = category.icon, fontSize = 20.sp)
@@ -512,7 +524,7 @@ fun BillAdditionalDetailsSection(
var pmExpanded by remember { mutableStateOf(false) } var pmExpanded by remember { mutableStateOf(false) }
val selectedPm = 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( EditableExposedDropdownMenu(
value = selectedPm?.name ?: "", value = selectedPm?.name ?: "",
@@ -541,7 +553,7 @@ fun BillAdditionalDetailsSection(
} }
paymentModes.forEach { pm -> paymentModes.forEach { pm ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.paymentModeId = if (pm.id > 0) pm.id else pm.remoteId viewModel.paymentModeId = pm.id
pmExpanded = false pmExpanded = false
}) { }) {
Text(text = pm.icon, fontSize = 20.sp) Text(text = pm.icon, fontSize = 20.sp)
@@ -2,22 +2,20 @@ package net.helcel.cowspent.android.bill_edit
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.android.helper.parseAmount import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath import net.helcel.cowspent.util.evalMath
import net.helcel.cowspent.model.DBCurrency
import androidx.compose.ui.graphics.vector.ImageVector
class EditBillViewModel : ViewModel() { class EditBillViewModel : ViewModel() {
var what by mutableStateOf("") var what by mutableStateOf("")
var amount by mutableStateOf("") var amount by mutableStateOf("")
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType 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 billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val allCategorized = 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) db.ensureDefaultLabels(projectId, projectType)
val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) { // Reload from DB to get real IDs
defaultCategories val categories = db.getCategories(projectId)
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT.toLong() })
}
val categories = syncedCategories + hardcoded
Quadruple(members, billsToLabel, categories, allCategorized) Quadruple(members, billsToLabel, categories, allCategorized)
} }
@@ -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) DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L)
) )
val cats = listOf( val cats = listOf(
DBCategory(-1, -1, 1, "Groceries", "🛒", ""), DBCategory(1L, 1L, 1L, "Groceries", "🛒", ""),
DBCategory(-2, -2, 1, "Leisure", "🥳", ""), DBCategory(2L, 2L, 1L, "Leisure", "🥳", ""),
DBCategory(-3, -3, 1, "Rent", "🏠", ""), DBCategory(3L, 3L, 1L, "Rent", "🏠", ""),
DBCategory(-4, -4, 1, "Bills", "💸", "") DBCategory(4L, 4L, 1L, "Bills", "💸", "")
) )
categories = cats categories = cats
categoriesMap = cats.associateBy { it.id } categoriesMap = cats.associateBy { it.id }
@@ -33,9 +33,11 @@ class ManageCurrenciesActivity : AppCompatActivity() {
if (message.isEmpty()) { if (message.isEmpty()) {
showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG) showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
} else { } else {
viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message), viewModel.showDialog(
message=getString(R.string.action_currencies), title = getString(R.string.dialog_sync_error_title),
positiveText = getString(android.R.string.ok)) message = getString(R.string.error_edit_remote_project_helper, message),
positiveText = getString(android.R.string.ok)
)
} }
} }
override fun onScheduled() {} override fun onScheduled() {}
@@ -28,6 +28,7 @@ import androidx.core.graphics.toColorInt
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.ColorPicker import net.helcel.cowspent.android.helper.ColorPicker
import net.helcel.cowspent.android.helper.StatefulAlertDialog import net.helcel.cowspent.android.helper.StatefulAlertDialog
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBPaymentMode
@@ -59,10 +60,10 @@ fun LabelManagementScreenContent(
dialogState: net.helcel.cowspent.android.helper.DialogState?, dialogState: net.helcel.cowspent.android.helper.DialogState?,
onBack: () -> Unit, onBack: () -> Unit,
onAddCategory: (String, String, String) -> Unit, onAddCategory: (String, String, String) -> Unit,
onUpdateCategory: (Long, String, String, String) -> Unit, onUpdateCategory: (DBCategory, String, String, String) -> Unit,
onDeleteCategory: (Long) -> Unit, onDeleteCategory: (Long) -> Unit,
onAddPaymentMode: (String, String, String) -> Unit, onAddPaymentMode: (String, String, String) -> Unit,
onUpdatePaymentMode: (Long, String, String, String) -> Unit, onUpdatePaymentMode: (DBPaymentMode, String, String, String) -> Unit,
onDeletePaymentMode: (Long) -> Unit, onDeletePaymentMode: (Long) -> Unit,
onDismissDialog: () -> Unit, onDismissDialog: () -> Unit,
onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit, onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit,
@@ -123,7 +124,7 @@ fun LabelManagementScreenContent(
if (selectedTab == 0) { if (selectedTab == 0) {
CategoryList( CategoryList(
categories = categories, categories = categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT },
onEdit = { onEdit = {
editingCategory = it editingCategory = it
showEditDialog = true showEditDialog = true
@@ -162,7 +163,7 @@ fun LabelManagementScreenContent(
if (showEditDialog) { if (showEditDialog) {
if (selectedTab == 0) { if (selectedTab == 0) {
EditLabelDialog( 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 ?: "", initialName = editingCategory?.name ?: "",
initialIcon = editingCategory?.icon ?: "", initialIcon = editingCategory?.icon ?: "",
initialColor = editingCategory?.color ?: "#FF0000", initialColor = editingCategory?.color ?: "#FF0000",
@@ -171,14 +172,14 @@ fun LabelManagementScreenContent(
if (editingCategory == null) { if (editingCategory == null) {
onAddCategory(name, icon, color) onAddCategory(name, icon, color)
} else { } else {
onUpdateCategory(editingCategory!!.id, name, icon, color) onUpdateCategory(editingCategory!!, name, icon, color)
} }
showEditDialog = false showEditDialog = false
} }
) )
} else { } else {
EditLabelDialog( 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 ?: "", initialName = editingPaymentMode?.name ?: "",
initialIcon = editingPaymentMode?.icon ?: "", initialIcon = editingPaymentMode?.icon ?: "",
initialColor = editingPaymentMode?.color ?: "#00FF00", initialColor = editingPaymentMode?.color ?: "#00FF00",
@@ -187,7 +188,7 @@ fun LabelManagementScreenContent(
if (editingPaymentMode == null) { if (editingPaymentMode == null) {
onAddPaymentMode(name, icon, color) onAddPaymentMode(name, icon, color)
} else { } else {
onUpdatePaymentMode(editingPaymentMode!!.id, name, icon, color) onUpdatePaymentMode(editingPaymentMode!!, name, icon, color)
} }
showEditDialog = false showEditDialog = false
} }
@@ -353,8 +354,8 @@ fun LabelManagementCategoriesPreview() {
MaterialTheme { MaterialTheme {
LabelManagementScreenContent( LabelManagementScreenContent(
categories = listOf( categories = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", "#FF0000"), DBCategory(1L, 1L, 1L, "Groceries", "🛒", "#FF0000"),
DBCategory(2, 2, 1, "Rent", "🏠", "#00FF00") DBCategory(2L, 2L, 1L, "Rent", "🏠", "#00FF00")
), ),
paymentModes = emptyList(), paymentModes = emptyList(),
dialogState = null, dialogState = null,
@@ -379,8 +380,8 @@ fun LabelManagementPaymentModesPreview() {
LabelManagementScreenContent( LabelManagementScreenContent(
categories = emptyList(), categories = emptyList(),
paymentModes = listOf( paymentModes = listOf(
DBPaymentMode(1, 1, 1, "Cash", "💵", "#0000FF"), DBPaymentMode(1L, 1L, 1L, "Cash", "💵", "#0000FF"),
DBPaymentMode(2, 2, 1, "Credit Card", "💳", "#FFFF00") DBPaymentMode(2L, 2L, 1L, "Credit Card", "💳", "#FFFF00")
), ),
dialogState = null, dialogState = null,
onBack = {}, onBack = {},
@@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext
import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
class LabelManagementViewModel(application: Application) : AndroidViewModel(application) { class LabelManagementViewModel(application: Application) : AndroidViewModel(application) {
private val db = CowspentSQLiteOpenHelper.getInstance(application) private val db = CowspentSQLiteOpenHelper.getInstance(application)
@@ -28,44 +28,30 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun loadLabels(projId: Long) { fun loadLabels(projId: Long) {
projectId = projId projectId = projId
viewModelScope.launch { viewModelScope.launch {
val cats = withContext(Dispatchers.IO) { db.getCategories(projId) } val project = withContext(Dispatchers.IO) { db.getProject(projId) }
val pms = withContext(Dispatchers.IO) { db.getPaymentModes(projId) } if (project != null) {
if (cats.isEmpty()) {
val defaults = CategoryUtils.getDefaultCategories(getApplication(), projId)
withContext(Dispatchers.IO) { 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) { fun addCategory(name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { 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) loadLabels(projectId)
} }
} }
fun updateCategory(id: Long, name: String, icon: String, color: String) { fun updateCategory(cat: DBCategory, name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.updateCategory(id, name, icon, color) db.updateCategoryAndSync(cat, name, icon, color)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -74,7 +60,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deleteCategory(id: Long) { fun deleteCategory(id: Long) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.deleteCategory(id) db.deleteCategoryAndSync(id)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -83,16 +69,16 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun addPaymentMode(name: String, icon: String, color: String) { fun addPaymentMode(name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { 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) loadLabels(projectId)
} }
} }
fun updatePaymentMode(id: Long, name: String, icon: String, color: String) { fun updatePaymentMode(pm: DBPaymentMode, name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.updatePaymentMode(id, name, icon, color) db.updatePaymentModeAndSync(pm, name, icon, color)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -101,7 +87,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deletePaymentMode(id: Long) { fun deletePaymentMode(id: Long) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.deletePaymentMode(id) db.deletePaymentModeAndSync(id)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -103,6 +103,10 @@ fun BillsListScreen(
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false) val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
var showBetaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
StatefulAlertDialog( StatefulAlertDialog(
state = viewModel.dialogState, state = viewModel.dialogState,
@@ -158,7 +162,8 @@ fun BillsListScreen(
isArchived = proj?.isArchived == true, isArchived = proj?.isArchived == true,
projectType = proj?.type ?: ProjectType.LOCAL, projectType = proj?.type ?: ProjectType.LOCAL,
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN, accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
isShareable = proj?.isShareable() ?: true isShareable = proj?.isShareable() ?: true,
showBetaFeatures = showBetaFeatures
) )
} }
} }
@@ -327,7 +332,7 @@ fun BillsListScreen(
}, },
actions = { actions = {
if (!isSearchExpanded) { if (!isSearchExpanded) {
if (viewModel.hasUnlabeledBills) { if (showBetaFeatures && viewModel.hasUnlabeledBills) {
IconButton(onClick = onLabelBillsClick) { IconButton(onClick = onLabelBillsClick) {
Icon( Icon(
Icons.Default.Category, Icons.Default.Category,
@@ -5,6 +5,7 @@ import android.content.Intent
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.*
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.IRefreshBillsListCallback import net.helcel.cowspent.util.IRefreshBillsListCallback
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -98,6 +99,10 @@ object BillsListUtils {
context: Context context: Context
) { ) {
val timestamp = System.currentTimeMillis() / 1000 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) { for (t in transactions) {
val owerId = t.owerMemberId val owerId = t.owerMemberId
val receiverId = t.receiverMemberId val receiverId = t.receiverMemberId
@@ -106,7 +111,7 @@ object BillsListUtils {
0, 0, projectId, owerId, amount, 0, 0, projectId, owerId, amount,
timestamp, context.getString(R.string.settle_bill_what), timestamp, context.getString(R.string.settle_bill_what),
DBBill.STATE_ADDED, DBBill.NON_REPEATED, DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, DBBill.PAYMODE_NONE, reimbursementCategoryId,
"", DBBill.PAYMODE_ID_NONE "", DBBill.PAYMODE_ID_NONE
) )
bill.billOwers += DBBillOwer(0, 0, receiverId) bill.billOwers += DBBillOwer(0, 0, receiverId)
@@ -21,9 +21,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.filled.Sync
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -50,6 +48,7 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.persistence.CowspentServerSyncHelper import net.helcel.cowspent.persistence.CowspentServerSyncHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillFormatter import net.helcel.cowspent.util.BillFormatter
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.CospendClientUtil import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.ExportUtil import net.helcel.cowspent.util.ExportUtil
import net.helcel.cowspent.util.ICallback import net.helcel.cowspent.util.ICallback
@@ -555,11 +554,13 @@ class BillsListViewActivity :
val members = db.getMembersOfProject(proj.id, null) val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id) val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId)
val balances = HashMap<Long, Double>() val balances = HashMap<Long, Double>()
SupportUtil.getStats( SupportUtil.getStats(
members, bills, members, bills,
mutableMapOf(), balances, mutableMapOf(), mutableMapOf(), mutableMapOf(), balances, mutableMapOf(), mutableMapOf(),
-1000, -1000, null, null -1000L, -1000L, reimbursementCategoryId, null, null
) )
Triple(proj, members, balances) 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() { private fun updateUsernameInDrawer() {
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) { if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) {
val text = getString(R.string.drawer_no_account) val text = getString(R.string.drawer_no_account)
@@ -149,16 +149,16 @@ object ProjectImportHelper {
val memberNameToId = mutableMapOf<String, Long>() val memberNameToId = mutableMapOf<String, Long>()
val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null)) val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null))
val pmRemoteToLocal = mutableMapOf<Long, Long>() val pmRemoteToLocal = mutableMapOf<Long, Long>()
paymentModes.forEach { pm -> paymentModes.forEach {
val localId = db.addPaymentMode(DBPaymentMode(0, pm.remoteId, pid, pm.name, pm.icon, pm.color)) val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color))
pmRemoteToLocal[pm.remoteId] = localId pmRemoteToLocal[it.remoteId] = newId
} }
val catRemoteToLocal = mutableMapOf<Long, Long>() val catRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach { cat -> categories.forEach {
val localId = db.addCategory(DBCategory(0, cat.remoteId, pid, cat.name, cat.icon, cat.color)) val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color))
catRemoteToLocal[cat.remoteId] = localId catRemoteToLocal[it.remoteId] = newId
} }
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) } currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
@@ -168,8 +168,8 @@ object ProjectImportHelper {
bills.forEach { b -> bills.forEach { b ->
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
val localCatId = catRemoteToLocal[b.categoryId] ?: b.categoryId val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: b.paymentModeId 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)) 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 -> billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) } memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) }
@@ -35,7 +35,8 @@ fun ProjectOptionsDialogContent(
isArchived: Boolean = false, isArchived: Boolean = false,
projectType: ProjectType = ProjectType.LOCAL, projectType: ProjectType = ProjectType.LOCAL,
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN, accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
isShareable: Boolean = true isShareable: Boolean = true,
showBetaFeatures: Boolean = false
) { ) {
Surface( Surface(
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
@@ -79,7 +80,7 @@ fun ProjectOptionsDialogContent(
// Row 2: Manage Member, Manage Labels, Manage Currencies // Row 2: Manage Member, Manage Labels, Manage Currencies
if (!isArchived && isMaintainer) { if (!isArchived && isMaintainer) {
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers)) row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
if (false && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) { if (showBetaFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels)) row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
} }
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies)) row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
@@ -181,7 +182,8 @@ fun ProjectOptionsDialogPreview() {
isArchived = false, isArchived = false,
projectType = ProjectType.COSPEND, projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -205,7 +207,8 @@ fun ProjectOptionsDialogPreview2() {
isArchived = true, isArchived = true,
projectType = ProjectType.COSPEND, projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -228,7 +231,8 @@ fun ProjectOptionsDialogPreview3() {
isArchived = false, isArchived = false,
projectType = ProjectType.LOCAL, projectType = ProjectType.LOCAL,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -320,7 +320,6 @@ class NewProjectActivity : AppCompatActivity() {
pid pid
) )
} }
showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG)
return pid return pid
} }
@@ -79,6 +79,7 @@ fun SettingsScreen(
val keyColor = stringResource(R.string.pref_key_color) val keyColor = stringResource(R.string.pref_key_color)
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode) val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
val keyShowArchived = stringResource(R.string.pref_key_show_archived) val keyShowArchived = stringResource(R.string.pref_key_show_archived)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
// States for preferences // States for preferences
var nightMode by remember(keyNightMode) { var nightMode by remember(keyNightMode) {
@@ -113,6 +114,9 @@ fun SettingsScreen(
var showArchived by remember(keyShowArchived) { var showArchived by remember(keyShowArchived) {
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false)) mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
} }
var betaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -242,6 +246,19 @@ fun SettingsScreen(
// Other // Other
SettingsCategory(stringResource(R.string.settings_other)) SettingsCategory(stringResource(R.string.settings_other))
SettingsSwitchPreference(
title = stringResource(R.string.settings_beta_features),
summary = stringResource(R.string.settings_beta_features_summary),
icon = Icons.Default.Info,
checked = betaFeatures,
onCheckedChange = {
betaFeatures = it
sharedPreferences.edit {
putBoolean(keyBetaFeatures, it)
}
}
)
SettingsPreference( SettingsPreference(
title = stringResource(R.string.title_about), title = stringResource(R.string.title_about),
icon = Icons.Default.Info, icon = Icons.Default.Info,
@@ -71,6 +71,7 @@ import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.CategoryUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -87,6 +88,8 @@ private object SankeyDimens {
@Composable @Composable
fun ProjectSankeyDiagram( fun ProjectSankeyDiagram(
projectName: String, projectName: String,
projectId: Long,
projectRemoteId: String?,
allMembers: List<DBMember>, allMembers: List<DBMember>,
allBills: List<DBBill>, allBills: List<DBBill>,
customCategories: List<DBCategory>, customCategories: List<DBCategory>,
@@ -96,8 +99,11 @@ fun ProjectSankeyDiagram(
var selectedMemberId by remember { mutableLongStateOf(-1L) } var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) { val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong() } 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 } } 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) @Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable @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()) {} }
@@ -84,6 +84,21 @@ fun ProjectStatisticsScreen(
val statsData by produceState<StatisticsData?>(null, proj.id) { val statsData by produceState<StatisticsData?>(null, proj.id) {
value = withContext(Dispatchers.IO) { 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 members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id) val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id) val categories = db.getCategories(proj.id)
@@ -94,19 +109,12 @@ fun ProjectStatisticsScreen(
if (statsData != null) { if (statsData != null) {
val data = statsData!! val data = statsData!!
val defaultCategories = remember(proj.id) { CategoryUtils.getDefaultCategories(context, proj.id) } val categories = data.categories
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 categoryNoneLabel = stringResource(R.string.category_none) 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") 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) { when (selectedTab) {
@@ -131,6 +139,8 @@ fun ProjectStatisticsScreen(
2 -> { 2 -> {
ProjectSankeyDiagram( ProjectSankeyDiagram(
projectName = proj.name.ifEmpty { proj.remoteId }, projectName = proj.name.ifEmpty { proj.remoteId },
projectId = proj.id,
projectRemoteId = proj.remoteId,
allMembers = data.members, allMembers = data.members,
allBills = data.bills, allBills = data.bills,
customCategories = sankeyCategories, customCategories = sankeyCategories,
@@ -44,8 +44,8 @@ fun ProjectStatisticsTable(
val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) } val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) }
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) } val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
var categoryId by remember { mutableIntStateOf(-1000) } var categoryId by remember { mutableLongStateOf(-1000L) }
var paymentModeId by remember { mutableIntStateOf(-1000) } var paymentModeId by remember { mutableLongStateOf(-1000L) }
var dateMin by remember { mutableStateOf<String?>(null) } var dateMin by remember { mutableStateOf<String?>(null) }
var dateMax by remember { mutableStateOf<String?>(null) } var dateMax by remember { mutableStateOf<String?>(null) }
@@ -61,10 +61,10 @@ fun ProjectStatisticsTable(
val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId }) val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId })
val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) { val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) {
val list = mutableListOf<Triple<Int, String, String>>() val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000, "📋", categoryAll)) list.add(Triple(-1000L, "📋", categoryAll))
list.add(Triple(-100, "🧾", categoryAllExceptReimbursement)) list.add(Triple(-100L, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0, "", categoryNone)) list.add(Triple(0L, "", categoryNone))
val catsToUse = if (proj.type == ProjectType.LOCAL) { val catsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultCategories(context, proj.id) CategoryUtils.getDefaultCategories(context, proj.id)
@@ -75,15 +75,15 @@ fun ProjectStatisticsTable(
} }
catsToUse.forEach { 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 } list.distinctBy { it.first }
} }
val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) { val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) {
val list = mutableListOf<Triple<Int, String, String>>() val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000, "💳", paymentModeAll)) list.add(Triple(-1000L, "💳", paymentModeAll))
list.add(Triple(0, "", paymentModeNone)) list.add(Triple(0L, "", paymentModeNone))
val pmsToUse = if (proj.type == ProjectType.LOCAL) { val pmsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultPaymentModes(context, proj.id) CategoryUtils.getDefaultPaymentModes(context, proj.id)
@@ -94,7 +94,7 @@ fun ProjectStatisticsTable(
} }
pmsToUse.forEach { 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 } list.distinctBy { it.first }
} }
@@ -105,10 +105,12 @@ fun ProjectStatisticsTable(
val membersPaid = HashMap<Long, Double>() val membersPaid = HashMap<Long, Double>()
val membersSpent = HashMap<Long, Double>() val membersSpent = HashMap<Long, Double>()
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(customCategories, proj.id, proj.remoteId)
SupportUtil.getStats( SupportUtil.getStats(
allMembers, allBills, allMembers, allBills,
membersNbBills, membersBalance, membersPaid, membersSpent, membersNbBills, membersBalance, membersPaid, membersSpent,
categoryId, paymentModeId, dateMin, dateMax categoryId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
) )
var statsText = shareStatsIntro + "\n\n" var statsText = shareStatsIntro + "\n\n"
@@ -1,6 +1,5 @@
package net.helcel.cowspent.model package net.helcel.cowspent.model
import android.util.Log
import java.io.Serializable import java.io.Serializable
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@@ -7,16 +7,22 @@ import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.main.BillsListViewActivity import net.helcel.cowspent.model.DBAccountProject
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBBillOwer
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SecureStorage import net.helcel.cowspent.util.SecureStorage
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.text.SimpleDateFormat import java.util.Locale
import java.util.*
/** /**
* Helps to add, get, update and delete bills, members, projects with the option to trigger a sync with the server. * Helps to add, get, update and delete bills, members, projects with the option to trigger a sync with the server.
@@ -156,10 +162,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
} }
@SuppressLint("Range") @SuppressLint("Range")
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
if (oldVersion < 1) {
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
recreateDatabase(db) recreateDatabase(db)
@@ -202,311 +205,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.execSQL("CREATE INDEX IF NOT EXISTS $indexName ON $table($key_id)") db.execSQL("CREATE INDEX IF NOT EXISTS $indexName ON $table($key_id)")
} }
fun addAccountProject(accountProject: DBAccountProject): Long { // --- Projects logic ---
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<DBAccountProject>
get() = getAccountProjectsCustom("", arrayOf(), default_order)
@WorkerThread
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBAccountProject> {
return getAccountProjectsCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> {
val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy)
val accountProjects: MutableList<DBAccountProject> = 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<DBPaymentMode> {
return getPaymentModesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null)
}
@WorkerThread
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBPaymentMode> {
return getPaymentModesCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> {
val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy)
val paymentModes: MutableList<DBPaymentMode> = 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<DBCategory> {
return getCategoriesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null)
}
@WorkerThread
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> {
return getCategoriesCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCategory> {
val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy)
val categories: MutableList<DBCategory> = 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<DBCurrency> {
return getCurrenciesCustom(
"$key_projectid = ? AND $key_state != ?",
arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()),
null
)
}
@WorkerThread
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCurrency> {
return getCurrenciesCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> {
val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy)
val currencies: MutableList<DBCurrency> = ArrayList()
while (cursor.moveToNext()) {
currencies.add(getCurrencyFromCursor(cursor))
}
cursor.close()
return currencies
}
@SuppressLint("Range")
private fun getCurrencyFromCursor(cursor: Cursor): DBCurrency {
return DBCurrency(
cursor.getLong(cursor.getColumnIndex(key_id)),
cursor.getLong(cursor.getColumnIndex(key_remoteId)),
cursor.getLong(cursor.getColumnIndex(key_projectid)),
cursor.getString(cursor.getColumnIndex(key_name)),
cursor.getDouble(cursor.getColumnIndex(key_exchangeRate)),
cursor.getInt(cursor.getColumnIndex(key_state))
)
}
fun setCurrencyStateSync(currencyId: Long, state: Int) {
setCurrencyState(currencyId, state)
val currency = getCurrency(currencyId)
if (currency != null) {
val project = getProject(currency.projectId)
if (project != null) syncIfRemote(project)
}
}
fun setCurrencyState(currencyId: Long, state: Int) {
val db = writableDatabase
val values = ContentValues()
values.put(key_state, state)
db.update(table_currencies, values, "$key_id = ?", arrayOf(currencyId.toString()))
}
fun addProject(project: DBProject): Long { fun addProject(project: DBProject): Long {
val db = writableDatabase val db = writableDatabase
@@ -571,16 +270,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( fun updateProject(
projId: Long, newName: String?, newEmail: String?, projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?, newPassword: String?, newLastPayerId: Long?,
@@ -647,8 +336,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 { fun addMember(m: DBMember): Long {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add member]") }
val db = writableDatabase val db = writableDatabase
val values = ContentValues() val values = ContentValues()
values.put(key_remoteId, m.remoteId) values.put(key_remoteId, m.remoteId)
@@ -705,7 +405,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
m.id, newName, newWeight, newActivated, newState, null, m.id, newName, newWeight, newActivated, newState, null,
newR, newG, newB, newNcUserId, newAvatar newR, newG, newB, newNcUserId, newAvatar
) )
Log.v(TAG, "UPDATE MEMBER AND SYNC")
val proj = getProject(m.projectId) val proj = getProject(m.projectId)
if (proj != null) syncIfRemote(proj) if (proj != null) syncIfRemote(proj)
} }
@@ -780,8 +479,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_members, "$key_id = ?", arrayOf(id.toString())) db.delete(table_members, "$key_id = ?", arrayOf(id.toString()))
} }
// --- Bills logic ---
fun addBill(b: DBBill): Long { fun addBill(b: DBBill): Long {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add bill]") }
val db = writableDatabase val db = writableDatabase
val values = ContentValues() val values = ContentValues()
values.put(key_remoteId, b.remoteId) values.put(key_remoteId, b.remoteId)
@@ -866,7 +566,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
} }
} }
} }
Log.v(TAG, "UPDATE BILL AND SYNC")
val proj = getProject(bill.projectId) val proj = getProject(bill.projectId)
if (proj != null) syncIfRemote(proj) if (proj != null) syncIfRemote(proj)
} }
@@ -887,29 +586,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
return getBillsCustom("$key_payer_id = ?", arrayOf(memberId.toString()), "$key_timestamp ASC") return getBillsCustom("$key_payer_id = ?", arrayOf(memberId.toString()), "$key_timestamp ASC")
} }
@Suppress("unused") fun getBill(id: Long): DBBill? {
fun getBill(remoteId: Long, projId: Long): DBBill? { val bills = getBillsCustom("$key_id = ?", arrayOf(id.toString()), null)
val bills = getBillsCustom(
"$key_remoteId = ? AND $key_projectid = ?",
arrayOf(remoteId.toString(), projId.toString()),
null
)
return if (bills.isEmpty()) null else bills[0] 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<DBCurrency> {
return getCurrenciesCustom(
"$key_projectid = ? AND $key_state = ?",
arrayOf(projId.toString(), state.toString()),
null
)
}
@WorkerThread @WorkerThread
fun searchBills(query: CharSequence?, projectId: Long): List<DBBill> { fun searchBills(query: CharSequence?, projectId: Long): List<DBBill> {
val andWhere: MutableList<String> = ArrayList() val andWhere: MutableList<String> = ArrayList()
@@ -1019,8 +700,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_bills, "$key_id = ?", arrayOf(id.toString())) db.delete(table_bills, "$key_id = ?", arrayOf(id.toString()))
} }
// --- Billowers logic ---
fun addBillower(billId: Long, memberId: Long) { fun addBillower(billId: Long, memberId: Long) {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add billower]") }
val db = writableDatabase val db = writableDatabase
val values = ContentValues() val values = ContentValues()
values.put(key_billId, billId) values.put(key_billId, billId)
@@ -1062,9 +744,445 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_billowers, "$key_id = ?", arrayOf(id.toString())) 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<DBCategory> {
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<DBCategory> {
return getCategoriesCustom(
"$key_projectid = ? AND $key_state = ?",
arrayOf(projId.toString(), state.toString()),
null
)
}
@WorkerThread
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> {
val db = readableDatabase
val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy)
val categories: MutableList<DBCategory> = 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<DBPaymentMode> {
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<DBPaymentMode> {
return getPaymentModesCustom(
"$key_projectid = ? AND $key_state = ?",
arrayOf(projId.toString(), state.toString()),
null
)
}
@WorkerThread
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBPaymentMode> {
return getPaymentModesCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> {
val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy)
val paymentModes: MutableList<DBPaymentMode> = 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<DBCurrency> {
return getCurrenciesCustom(
"$key_projectid = ? AND $key_state != ?",
arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()),
null
)
}
fun getCurrenciesOfProjectWithState(projId: Long, state: Int): List<DBCurrency> {
return getCurrenciesCustom(
"$key_projectid = ? AND $key_state = ?",
arrayOf(projId.toString(), state.toString()),
null
)
}
@WorkerThread
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCurrency> {
return getCurrenciesCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> {
val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy)
val currencies: MutableList<DBCurrency> = 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<DBAccountProject>
get() = getAccountProjectsCustom("", arrayOf(), default_order)
@WorkerThread
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBAccountProject> {
return getAccountProjectsCustom(selection, selectionArgs, orderBy, readableDatabase)
}
@WorkerThread
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> {
val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy)
val accountProjects: MutableList<DBAccountProject> = 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") @Suppress("ConstPropertyName")
companion object { companion object {
private val TAG = CowspentSQLiteOpenHelper::class.java.simpleName
private const val database_version = 1 private const val database_version = 1
private const val database_name = "COWSPENT" private const val database_name = "COWSPENT"
private const val table_members = "MEMBERS" private const val table_members = "MEMBERS"
@@ -1100,16 +1218,16 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
private const val key_repeat = "REPEAT" private const val key_repeat = "REPEAT"
private const val key_payment_mode = "PAYMENTMODE" private const val key_payment_mode = "PAYMENTMODE"
private const val key_payment_mode_id = "PAYMENTMODEID" private const val key_payment_mode_id = "PAYMENTMODEID"
private const val key_category_id = "CATEGORYID" const val key_category_id = "CATEGORYID"
private const val key_comment = "COMMENT" const val key_comment = "COMMENT"
private const val table_billowers = "BILLOWERS" private const val table_billowers = "BILLOWERS"
private const val key_billId = "BILLID" private const val key_billId = "BILLID"
private const val key_member_id = "MEMBERID" private const val key_member_id = "MEMBERID"
private const val table_account_projects = "ACCOUNTPROJECTS" private const val table_account_projects = "ACCOUNTPROJECTS"
private const val key_ncUrl = "NCURL" private const val key_ncUrl = "NCURL"
private const val table_categories = "CATEGORIES" private const val table_categories = "CATEGORIES"
private const val key_icon = "ICON" const val key_icon = "ICON"
private const val key_color = "COLOR" const val key_color = "COLOR"
private const val table_payment_modes = "PAYMENTMODES" private const val table_payment_modes = "PAYMENTMODES"
private const val key_latest_bill_ts = "LATEST_BILL_TS" private const val key_latest_bill_ts = "LATEST_BILL_TS"
private const val table_currencies = "CURRENCIES" private const val table_currencies = "CURRENCIES"
@@ -264,10 +264,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val members = dbHelper.getMembersOfProject(project.id, null) val members = dbHelper.getMembersOfProject(project.id, null)
val memberIdToRemoteId = members.associate { it.id to it.remoteId } 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 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 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) val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (bToDel in toDelete) { for (bToDel in toDelete) {
@@ -298,11 +409,15 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
for (bToEdit in toEdit) { for (bToEdit in toEdit) {
try { try {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId) 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) 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 { } else {
Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})") Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)")
} }
} catch (_: Exception) { } catch (_: Exception) {
Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely") Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely")
@@ -312,7 +427,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED) val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (bToAdd in toAdd) { for (bToAdd in toAdd) {
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId) val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val newRemoteId = createRemoteBillResponse.stringContent.toLong() val newRemoteId = createRemoteBillResponse.remoteBillId
if (newRemoteId > 0) { if (newRemoteId > 0) {
dbHelper.updateBill( dbHelper.updateBill(
bToAdd.id, newRemoteId, null, bToAdd.id, newRemoteId, null,
@@ -379,7 +494,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED) val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) { for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd) val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong() val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId
if (newRemoteId > 0) { if (newRemoteId > 0) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK) dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
} }
@@ -475,7 +590,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localPaymentModes = dbHelper.getPaymentModes(project.id) val localPaymentModes = dbHelper.getPaymentModes(project.id)
for (localPaymentMode in localPaymentModes) { for (localPaymentMode in localPaymentModes) {
if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) { if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
dbHelper.deletePaymentMode(localPaymentMode.id) dbHelper.deletePaymentMode(localPaymentMode.id)
Log.d(TAG, "Delete local pm : $localPaymentMode") Log.d(TAG, "Delete local pm : $localPaymentMode")
} }
@@ -485,6 +600,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId } val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId }
for (c in remoteCategories) { for (c in remoteCategories) {
if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue
val localCategory = dbHelper.getCategory(c.remoteId, project.id) val localCategory = dbHelper.getCategory(c.remoteId, project.id)
if (localCategory == null) { if (localCategory == null) {
Log.d(TAG, "Add local category : $c") Log.d(TAG, "Add local category : $c")
@@ -504,7 +620,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCategories = dbHelper.getCategories(project.id) val localCategories = dbHelper.getCategories(project.id)
for (localCategory in localCategories) { for (localCategory in localCategories) {
if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) { if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
dbHelper.deleteCategory(localCategory.id) dbHelper.deleteCategory(localCategory.id)
Log.d(TAG, "Delete local category : $localCategory") Log.d(TAG, "Delete local category : $localCategory")
} }
@@ -532,7 +648,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCurrencies = dbHelper.getCurrencies(project.id) val localCurrencies = dbHelper.getCurrencies(project.id)
for (localCurrency in localCurrencies) { for (localCurrency in localCurrencies) {
if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) { if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
dbHelper.deleteCurrency(localCurrency.id) dbHelper.deleteCurrency(localCurrency.id)
Log.d(TAG, "Delete local currency : $localCurrencies") Log.d(TAG, "Delete local currency : $localCurrencies")
} }
@@ -599,13 +715,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val dbMembers = dbHelper.getMembersOfProject(project.id, null) val dbMembers = dbHelper.getMembersOfProject(project.id, null)
val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id } 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 billsResponse = client!!.getBills(project)
val isIHM = project.type == ProjectType.IHATEMONEY val isIHM = project.type == ProjectType.IHATEMONEY
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
val remoteBills: List<DBBill> = if (isIHM) { val remoteBills: List<DBBill> = if (isIHM) {
billsResponse.getBillsIHM(project.id, memberRemoteIdToId) billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} else { } else {
billsResponse.getBillsCospend(project.id, memberRemoteIdToId) billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} }
val remoteAllBillIds: List<Long> = if (isIHM) { val remoteAllBillIds: List<Long> = if (isIHM) {
remoteBills.map { it.remoteId } remoteBills.map { it.remoteId }
@@ -617,20 +743,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localBills = dbHelper.getBillsOfProject(project.id) val localBills = dbHelper.getBillsOfProject(project.id)
val localBillsByRemoteId = localBills.associateBy { it.remoteId } 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) { 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)) { if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) {
dbHelper.addBill(remoteBill) dbHelper.addBill(remoteBill)
nbPulledNewBills++ nbPulledNewBills++
newBillsDialogText += "+ ${remoteBill.what}\n" newBillsDialogText += "+ ${remoteBill.what}\n"
Log.d(TAG, "Add local bill : $remoteBill")
} else { } else {
val localBill = localBillsByRemoteId[remoteBill.remoteId]!! val localBill = localBillsByRemoteId[remoteBill.remoteId]!!
if (hasChanged(localBill, remoteBill)) { if (hasChanged(localBill, remoteBill)) {
@@ -643,7 +760,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
) )
nbPulledUpdatedBills++ nbPulledUpdatedBills++
updatedBillsDialogText += "${remoteBill.what}\n" updatedBillsDialogText += "${remoteBill.what}\n"
Log.d(TAG, "Update local bill : $remoteBill")
} else { } else {
Log.d(TAG, "Nothing to do for bill : $localBill") Log.d(TAG, "Nothing to do for bill : $localBill")
} }
@@ -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") 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<DBCategory>, 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 }
}
} }
@@ -93,6 +93,10 @@ class NextcloudClient(
params: Collection<QueryParam>?, params: Collection<QueryParam>?,
isOCSRequest: Boolean isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): 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 result = StringBuilder()
val headers: MutableMap<String, List<String>> = HashMap() val headers: MutableMap<String, List<String>> = HashMap()
if (isOCSRequest) { if (isOCSRequest) {
@@ -103,13 +107,13 @@ class NextcloudClient(
val nextcloudRequest: NextcloudRequest = if (params == null) { val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setHeader(headers) .setHeader(headers)
.build() .build()
} else { } else {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setParameter(params) .setParameter(params)
.setHeader(headers) .setHeader(headers)
.build() .build()
@@ -182,8 +186,12 @@ class NextcloudClient(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): 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 result = StringBuilder()
val targetURL = url + target.replace("^/".toRegex(), "") val targetURL = url + finalTarget.replace("^/".toRegex(), "")
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL") // Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
val httpCon = SupportUtil.getHttpURLConnection(targetURL) val httpCon = SupportUtil.getHttpURLConnection(targetURL)
httpCon.requestMethod = method httpCon.requestMethod = method
@@ -194,7 +202,7 @@ class NextcloudClient(
) )
} }
httpCon.setRequestProperty("Connection", "Close") 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) { if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag) httpCon.setRequestProperty("If-None-Match", lastETag)
} }
@@ -244,8 +252,12 @@ class NextcloudClient(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): 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 var strBase64: String
val targetURL = url + target.replace("^/".toRegex(), "") val targetURL = url + finalTarget.replace("^/".toRegex(), "")
val httpCon = SupportUtil.getHttpURLConnection( targetURL) val httpCon = SupportUtil.getHttpURLConnection( targetURL)
httpCon.requestMethod = method httpCon.requestMethod = method
if (needLogin) { if (needLogin) {
@@ -137,6 +137,90 @@ open class ServerResponse(
getResponseStringData().toLong() 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( class CreateRemoteCurrencyResponse(
response: VersatileProjectSyncClient.ResponseData, response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean isOcsResponse: Boolean
@@ -145,6 +229,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() 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( class EditRemoteCurrencyResponse(
@@ -196,6 +292,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() 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( class CreateRemoteBillResponse(
@@ -206,6 +314,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() 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( class DeleteRemoteBillResponse(
@@ -242,13 +362,23 @@ open class ServerResponse(
ServerResponse(response, isOcsResponse) { ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getBillsCospend(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> { fun getBillsCospend(
return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId) projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
return getBillsFromJSONObject(projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getBillsIHM(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> { fun getBillsIHM(
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId) projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
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<DBCategory> {
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<DBPaymentMode> {
return if (isOcsResponse) {
getPaymentModesFromJSON(getResponseObjectData(), projId)
} else {
getPaymentModesFromJSONArray(getResponseArrayData(), projId)
}
}
}
class AccountProjectsResponse( class AccountProjectsResponse(
response: VersatileProjectSyncClient.ResponseData, response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean isOcsResponse: Boolean
@@ -366,6 +522,26 @@ open class ServerResponse(
return members return members
} }
@Throws(JSONException::class)
protected fun getCategoriesFromJSONArray(jsonCats: JSONArray, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = 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<DBPaymentMode> {
val paymentModes: MutableList<DBPaymentMode> = ArrayList()
for (i in 0 until jsonPms.length()) {
val jsonPm = jsonPms.getJSONObject(i)
paymentModes.add(getPaymentModeFromJSON(jsonPm, projId))
}
return paymentModes
}
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> { protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList() val categories: MutableList<DBCategory> = ArrayList()
@@ -382,6 +558,27 @@ open class ServerResponse(
return categories 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) @Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory { protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory {
val remoteId = remoteIdStr.toLong() val remoteId = remoteIdStr.toLong()
@@ -416,6 +613,27 @@ open class ServerResponse(
return paymentModes 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) @Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode { protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode {
val remoteId = remoteIdStr.toLong() val remoteId = remoteIdStr.toLong()
@@ -556,26 +774,30 @@ open class ServerResponse(
protected fun getBillsFromJSONArray( protected fun getBillsFromJSONArray(
json: JSONArray, json: JSONArray,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> { ): List<DBBill> {
val bills: MutableList<DBBill> = ArrayList() val bills: MutableList<DBBill> = ArrayList()
for (i in 0 until json.length()) { for (i in 0 until json.length()) {
val jsonBill = json.getJSONObject(i) val jsonBill = json.getJSONObject(i)
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId)) bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId))
} }
return bills return bills
} }
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getBillsFromJSONObject( protected fun getBillsFromJSONObject(
json: JSONObject,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> { ): List<DBBill> {
val bills: List<DBBill> val bills: List<DBBill>
val json = getResponseObjectData()
if (json.has("bills") && !json.isNull("bills")) { if (json.has("bills") && !json.isNull("bills")) {
val jsonBills = json.getJSONArray("bills") val jsonBills = json.getJSONArray("bills")
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId) bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} else { } else {
bills = ArrayList() bills = ArrayList()
} }
@@ -586,7 +808,9 @@ open class ServerResponse(
protected fun getBillFromJSON( protected fun getBillFromJSON(
json: JSONObject, json: JSONObject,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): DBBill { ): DBBill {
var remoteId: Long = 0 var remoteId: Long = 0
var payerRemoteId: Long var payerRemoteId: Long
@@ -599,8 +823,8 @@ open class ServerResponse(
var comment = "" var comment = ""
var repeat = DBBill.NON_REPEATED var repeat = DBBill.NON_REPEATED
var paymentMode = DBBill.PAYMODE_NONE var paymentMode = DBBill.PAYMODE_NONE
var paymentModeId = DBBill.PAYMODE_ID_NONE.toLong() var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE
var categoryId = DBBill.CATEGORY_NONE.toLong() var categoryRemoteId = DBBill.CATEGORY_NONE
if (!json.isNull("id")) { if (!json.isNull("id")) {
remoteId = json.getLong("id") remoteId = json.getLong("id")
} }
@@ -639,20 +863,26 @@ open class ServerResponse(
} }
if (json.has("paymentmode") && !json.isNull("paymentmode")) { if (json.has("paymentmode") && !json.isNull("paymentmode")) {
paymentMode = json.getString("paymentmode") paymentMode = json.getString("paymentmode")
} else if (json.has("paymentMode") && !json.isNull("paymentMode")) {
paymentMode = json.getString("paymentMode")
} }
if (json.has("categoryid") && !json.isNull("categoryid")) { if (json.has("categoryid") && !json.isNull("categoryid")) {
categoryId = json.getLong("categoryid") categoryRemoteId = json.getLong("categoryid")
Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId") } else if (json.has("categoryId") && !json.isNull("categoryId")) {
categoryRemoteId = json.getLong("categoryId")
} }
if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) { 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 if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) {
// new MB, old Cospend => set payment mode ID from old one paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
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()
} }
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
val bill = DBBill( val bill = DBBill(
0, remoteId, projId, payerId, amount, timestamp, what, 0, remoteId, projId, payerId, amount, timestamp, what,
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId
@@ -87,14 +87,17 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>, membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>, membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>, membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int, catId: Long, paymentModeId: Long,
dateMin: String?, dateMax: String? dateMin: String?, dateMax: String?
): Int { ): Int {
val proj = db.getProject(projId)
val categories = db.getCategories(projId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projId, proj?.remoteId)
return getStats( return getStats(
db.getMembersOfProject(projId, null), db.getMembersOfProject(projId, null),
db.getBillsOfProject(projId), db.getBillsOfProject(projId),
membersNbBills, membersBalance, membersPaid, membersSpent, membersNbBills, membersBalance, membersPaid, membersSpent,
catId, paymentModeId, dateMin, dateMax catId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
) )
} }
@@ -106,7 +109,8 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>, membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>, membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>, membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int, catId: Long, paymentModeId: Long,
reimbursementCategoryId: Long,
dateMin: String?, dateMax: String? dateMin: String?, dateMax: String?
): Int { ): Int {
val nbBillsTotal = 0 val nbBillsTotal = 0
@@ -124,9 +128,9 @@ object SupportUtil {
for (b in dbBills) { for (b in dbBills) {
// don't take deleted bills and respect category filter // don't take deleted bills and respect category filter
if (b.state != DBBill.STATE_DELETED && if (b.state != DBBill.STATE_DELETED &&
((catId == -1000 || catId == -100 || b.categoryId == catId.toLong()) && ((catId == -1000L || catId == -100L || b.categoryId == catId) &&
(catId != -100 || b.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong()) && (catId != -100L || b.categoryId != reimbursementCategoryId) &&
(paymentModeId == -1000 || b.paymentModeId == paymentModeId.toLong())) && (paymentModeId == -1000L || b.paymentModeId == paymentModeId)) &&
(dateMin == null || b.date >= dateMin) && (dateMin == null || b.date >= dateMin) &&
(dateMax == null || b.date <= dateMax) (dateMax == null || b.date <= dateMax)
) { ) {
@@ -285,18 +289,6 @@ object SupportUtil {
return reduceBalance(crediters, debiters, results) 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 @JvmStatic
fun getJsonObject(text: String?): JSONObject? { fun getJsonObject(text: String?): JSONObject? {
if (text == null) return null if (text == null) return null
@@ -11,8 +11,10 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
import com.nextcloud.android.sso.exceptions.TokenMismatchException import com.nextcloud.android.sso.exceptions.TokenMismatchException
import com.nextcloud.android.sso.model.SingleSignOnAccount import com.nextcloud.android.sso.model.SingleSignOnAccount
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.model.ProjectType
import org.json.JSONException import org.json.JSONException
@@ -82,13 +84,11 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) { } else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) { target = if (cospendVersionGT161)
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true) else
} else { "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId return ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false)
}
} else { } else {
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161) target = if (cospendVersionGT161)
@@ -313,10 +313,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "") payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor) paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "") paramValues.add(bill.paymentMode ?: "")
val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0 paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0 paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
paramValues.add(remoteCatId.toString())
paramValues.add(remotePmId.toString())
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
@@ -568,10 +566,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "") payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor) paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "") paramValues.add(bill.paymentMode ?: "")
val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0 paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0 paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
paramValues.add(remoteCatId.toString())
paramValues.add(remotePmId.toString())
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
@@ -650,7 +646,6 @@ class VersatileProjectSyncClient(
} else if (canAccessProjectWithSSO(project)) { } else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) { return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members" 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) ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true)
} else { } else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members" target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members"
@@ -714,7 +709,6 @@ class VersatileProjectSyncClient(
paramValues.add(tsLastSync.toString()) paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) { return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" 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) ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
} else { } else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills" 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) @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getMembers(project: DBProject): ServerResponse.MembersResponse { fun getMembers(project: DBProject): ServerResponse.MembersResponse {
var target: String 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<String> = ArrayList()
val paramValues: MutableList<String> = 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<String> = ArrayList()
val paramValues: MutableList<String> = 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<String> = ArrayList()
val paramValues: MutableList<String> = 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<String> = ArrayList()
val paramValues: MutableList<String> = 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) @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse { fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse {
val paramKeys: MutableList<String> = ArrayList() val paramKeys: MutableList<String> = ArrayList()
@@ -952,6 +1331,10 @@ class VersatileProjectSyncClient(
nextcloudAPI: NextcloudAPI, target: String, method: String, nextcloudAPI: NextcloudAPI, target: String, method: String,
paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean
): ResponseData { ): 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 result = StringBuilder()
var params: MutableList<QueryParam>? = null var params: MutableList<QueryParam>? = null
if (paramKeys != null && paramValues != null) { if (paramKeys != null && paramValues != null) {
@@ -969,13 +1352,13 @@ class VersatileProjectSyncClient(
val nextcloudRequest: NextcloudRequest = if (params == null) { val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setHeader(headers) .setHeader(headers)
.build() .build()
} else { } else {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setParameter(params) .setParameter(params)
.setHeader(headers) .setHeader(headers)
.build() .build()
@@ -1009,8 +1392,12 @@ class VersatileProjectSyncClient(
lastETag: String?, username: String?, password: String?, lastETag: String?, username: String?, password: String?,
bearerToken: String?, isOCSRequest: Boolean bearerToken: String?, isOCSRequest: Boolean
): ResponseData { ): 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 result = StringBuilder()
val httpCon = SupportUtil.getHttpURLConnection(target) val httpCon = SupportUtil.getHttpURLConnection(finalTarget)
httpCon.requestMethod = method httpCon.requestMethod = method
if (bearerToken != null) { if (bearerToken != null) {
httpCon.setRequestProperty("Authorization", "Bearer $bearerToken") httpCon.setRequestProperty("Authorization", "Bearer $bearerToken")
@@ -1021,7 +1408,7 @@ class VersatileProjectSyncClient(
) )
} }
httpCon.setRequestProperty("Connection", "Close") 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) { if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag) httpCon.setRequestProperty("If-None-Match", lastETag)
} }
+6 -1
View File
@@ -35,6 +35,8 @@
<string name="title_settle">Settle Project</string> <string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string> <string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string> <string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string> <string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string> <string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string> <string name="title_share_qr">Cowspent link</string>
@@ -133,6 +135,8 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string> <string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
<string name="settings_colorpicker_title">Choose Color</string> <string name="settings_colorpicker_title">Choose Color</string>
@@ -153,6 +157,7 @@
<string name="pref_key_color_mode" translatable="false">colorMode</string> <string name="pref_key_color_mode" translatable="false">colorMode</string>
<string name="pref_key_offline_mode" translatable="false">offlineMode</string> <string name="pref_key_offline_mode" translatable="false">offlineMode</string>
<string name="pref_key_show_archived" translatable="false">showArchived</string> <string name="pref_key_show_archived" translatable="false">showArchived</string>
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
<string name="pref_value_night_mode_no" translatable="false">1</string> <string name="pref_value_night_mode_no" translatable="false">1</string>
<string name="pref_value_night_mode_yes" translatable="false">2</string> <string name="pref_value_night_mode_yes" translatable="false">2</string>
<string name="pref_value_night_mode_system" translatable="false">-1</string> <string name="pref_value_night_mode_system" translatable="false">-1</string>
@@ -229,7 +234,7 @@
<string name="settle_bill_what">Settlement</string> <string name="settle_bill_what">Settlement</string>
<!-- Currencies --> <!-- Currencies -->
<string name="currency_dialog_title">Choose Currency</string> <string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string> <string name="setting_none">None</string>
<string name="setting_all">All</string> <string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string> <string name="currency_saved_success">Currency settings saved.</string>
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}