4 Commits

Author SHA1 Message Date
soraefir 47d0471b48 Label sync and workflows 2026-07-05 15:02:36 +02:00
sora 56968dbe3b Update metadata/en-US/full_description.txt 2026-07-02 03:05:16 +02:00
soraefir 56036d5840 Fix toast & minor category/paymentmode fix 2026-07-02 02:43:33 +02:00
soraefir 4fa29da79d Better category/paymentmode storage and handling 2026-07-02 02:37:40 +02:00
29 changed files with 1573 additions and 656 deletions
@@ -26,7 +26,6 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil
import java.text.ParseException
import java.time.ZoneId
@@ -52,23 +51,10 @@ class EditBillActivity : AppCompatActivity() {
ThemeUtils.CowspentTheme {
val categories = remember {
val syncedCategories = db.getCategories(bill.projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@EditBillActivity, bill.projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
syncedCategories + hardcoded
db.getCategories(bill.projectId)
}
val paymentModes = remember {
val syncedPaymentModes = db.getPaymentModes(bill.projectId)
val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId)
if (projectType == ProjectType.LOCAL) {
syncedPaymentModes + defaultPaymentModes
} else {
syncedPaymentModes.ifEmpty { defaultPaymentModes }
}
db.getPaymentModes(bill.projectId)
}
EditBillScreen(
@@ -144,7 +130,7 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
first.id, 0, first.projectId, first.payerId, totalAmount,
first.timestamp, first.what, first.state, first.repeat,
first.paymentMode, first.categoryRemoteId, first.comment, first.paymentModeRemoteId
first.paymentMode, first.categoryId, first.comment, first.paymentModeId
)
val splits = mutableMapOf<Long, Double>()
@@ -175,8 +161,8 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
0, 0, projectId, btd.payerId, btd.amount,
timeNowSeconds, btd.what, DBBill.STATE_ADDED,
btd.repeat, btd.paymentMode, btd.categoryRemoteId,
btd.comment, btd.paymentModeRemoteId
btd.repeat, btd.paymentMode, btd.categoryId,
btd.comment, btd.paymentModeId
)
val btdOwers = btd.billOwers
val newBillOwers = btdOwers.filter {
@@ -195,6 +181,11 @@ class EditBillActivity : AppCompatActivity() {
val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId)
if (project != null) {
db.ensureDefaultLabels(bill.projectId, project.type)
}
withContext(Dispatchers.Main) {
viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: ""
@@ -239,8 +230,8 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
0, 0, bill.projectId, viewModel.payerId, viewModel.amountAsDouble,
System.currentTimeMillis() / 1000, viewModel.what, DBBill.STATE_ADDED,
viewModel.repeat, bill.paymentMode, viewModel.categoryRemoteId,
viewModel.getFinalComment(), viewModel.paymentModeRemoteId
viewModel.repeat, bill.paymentMode, viewModel.categoryId,
viewModel.getFinalComment(), viewModel.paymentModeId
)
calendar.timeInMillis = System.currentTimeMillis()
viewModel.timestamp = calendar.timeInMillis / 1000
@@ -326,8 +317,8 @@ class EditBillActivity : AppCompatActivity() {
bill.payerId == viewModel.payerId &&
bill.comment == viewModel.getFinalComment() &&
bill.repeat == viewModel.repeat &&
bill.categoryRemoteId == viewModel.categoryRemoteId &&
bill.paymentModeRemoteId == viewModel.paymentModeRemoteId &&
bill.categoryId == viewModel.categoryId &&
bill.paymentModeId == viewModel.paymentModeId &&
!owersChanged)
}
@@ -386,8 +377,8 @@ class EditBillActivity : AppCompatActivity() {
listOf(memberId),
viewModel.repeat,
existingBill.paymentMode,
viewModel.paymentModeRemoteId,
viewModel.categoryRemoteId,
viewModel.paymentModeId,
viewModel.categoryId,
finalComment
)
if (firstSavedId == 0L) firstSavedId = billToUseId
@@ -396,9 +387,11 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, amount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
)
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
newBill.categoryId = viewModel.categoryId
newBill.paymentModeId = viewModel.paymentModeId
val newId = db.addBill(newBill)
if (firstSavedId == 0L) firstSavedId = newId
}
@@ -441,8 +434,8 @@ class EditBillActivity : AppCompatActivity() {
newOwersIds,
viewModel.repeat,
bill.paymentMode,
viewModel.paymentModeRemoteId,
viewModel.categoryRemoteId,
viewModel.paymentModeId,
viewModel.categoryId,
finalComment
)
if (groupedBillIds != null) {
@@ -460,7 +453,7 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, newAmount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
)
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
val newBillId = db.addBill(newBill)
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.*
import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil
import java.util.Date
import kotlin.math.abs
@@ -466,9 +467,10 @@ fun BillAdditionalDetailsSection(
modifier = Modifier.padding(bottom = 8.dp, top = 16.dp)
)
val context = androidx.compose.ui.platform.LocalContext.current
var categoryExpanded by remember { mutableStateOf(false) }
val selectedCategory =
categories.find { it.remoteId.toInt() == viewModel.categoryRemoteId }
categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId)
EditableExposedDropdownMenu(
value = selectedCategory?.name ?: "",
@@ -488,16 +490,26 @@ fun BillAdditionalDetailsSection(
},
content = {
DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = 0
viewModel.categoryId = 0
categoryExpanded = false
}) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_none))
}
categories.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT
categoryExpanded = false
}) {
Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_reimbursement))
}
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = category.remoteId.toInt()
viewModel.categoryId = category.id
categoryExpanded = false
}) {
Text(text = category.icon, fontSize = 20.sp)
@@ -512,7 +524,7 @@ fun BillAdditionalDetailsSection(
var pmExpanded by remember { mutableStateOf(false) }
val selectedPm =
paymentModes.find { it.remoteId.toInt() == viewModel.paymentModeRemoteId }
paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId)
EditableExposedDropdownMenu(
value = selectedPm?.name ?: "",
@@ -532,7 +544,7 @@ fun BillAdditionalDetailsSection(
},
content = {
DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = 0
viewModel.paymentModeId = 0
pmExpanded = false
}) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
@@ -541,7 +553,7 @@ fun BillAdditionalDetailsSection(
}
paymentModes.forEach { pm ->
DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = pm.remoteId.toInt()
viewModel.paymentModeId = pm.id
pmExpanded = false
}) {
Text(text = pm.icon, fontSize = 20.sp)
@@ -25,8 +25,8 @@ class EditBillViewModel : ViewModel() {
var timestamp by mutableLongStateOf(0L)
var payerId by mutableLongStateOf(0L)
var repeat by mutableStateOf(DBBill.NON_REPEATED)
var paymentModeRemoteId by mutableIntStateOf(0)
var categoryRemoteId by mutableIntStateOf(0)
var paymentModeId by mutableLongStateOf(0L)
var categoryId by mutableLongStateOf(0L)
var isNewBill by mutableStateOf(false)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
@@ -166,8 +166,8 @@ class EditBillViewModel : ViewModel() {
timestamp = bill.timestamp
payerId = bill.payerId
repeat = bill.repeat ?: DBBill.NON_REPEATED
paymentModeRemoteId = bill.paymentModeRemoteId
categoryRemoteId = bill.categoryRemoteId
paymentModeId = bill.paymentModeId
categoryId = bill.categoryId
val rawComment = bill.comment ?: ""
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType
@@ -39,24 +38,20 @@ class LabelBillsActivity : AppCompatActivity() {
val members = db.getMembersOfProject(projectId, null)
val allBills = db.getBillsOfProject(projectId)
val billsToLabel = allBills.filter { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryRemoteId != 0 && it.state != DBBill.STATE_DELETED }
val billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryId != 0L && it.state != DBBill.STATE_DELETED }
val syncedCategories = db.getCategories(projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
val categories = syncedCategories + hardcoded
db.ensureDefaultLabels(projectId, projectType)
// Reload from DB to get real IDs
val categories = db.getCategories(projectId)
Quadruple(members, billsToLabel, categories, allCategorized)
}
viewModel.billsToLabel = billsToLabel
viewModel.categories = categories
viewModel.categoriesMap = categories.associateBy { it.remoteId }
viewModel.categoriesMap = categories.associateBy { it.id }
viewModel.allCategorizedBills = allCategorized
viewModel.updateSuggestions()
@@ -84,7 +84,7 @@ fun LabelBillsScreen(
CategoryButton(
icon = category.icon,
name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) }
onClick = { viewModel.labelCurrentBill(db, category.id) }
)
}
}
@@ -126,7 +126,7 @@ fun LabelBillsScreen(
CategoryButton(
icon = category.icon,
name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) }
onClick = { viewModel.labelCurrentBill(db, category.id) }
)
}
}
@@ -245,16 +245,16 @@ fun CategoryButton(icon: String, name: String, onClick: () -> Unit) {
fun LabelBillsScreenPreview() {
val viewModel = LabelBillsViewModel().apply {
billsToLabel = listOf(
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0, null, -1)
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L)
)
val cats = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", ""),
DBCategory(2, 2, 1, "Leisure", "🥳", ""),
DBCategory(3, 3, 1, "Rent", "🏠", ""),
DBCategory(4, 4, 1, "Bills", "💸", "")
DBCategory(1L, 1L, 1L, "Groceries", "🛒", ""),
DBCategory(2L, 2L, 1L, "Leisure", "🥳", ""),
DBCategory(3L, 3L, 1L, "Rent", "🏠", ""),
DBCategory(4L, 4L, 1L, "Bills", "💸", "")
)
categories = cats
categoriesMap = cats.associateBy { it.remoteId }
categoriesMap = cats.associateBy { it.id }
}
val members = listOf(
DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
@@ -43,18 +43,18 @@ class LabelBillsViewModel : ViewModel() {
otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName))
}
val counts = matches.groupBy { it.categoryRemoteId }
val counts = matches.groupBy { it.categoryId }
.mapValues { it.value.size }
.toList()
.sortedByDescending { it.second }
.take(2)
suggestedCategories = counts.mapNotNull { (catId, _) ->
categoriesMap[catId.toLong()]
categoriesMap[catId]
}
}
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Int) {
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) {
currentBill?.let { bill ->
db.updateBillAndSync(
bill = bill,
@@ -65,11 +65,11 @@ class LabelBillsViewModel : ViewModel() {
newOwersIds = bill.billOwersIds,
newRepeat = bill.repeat,
newPaymentMode = bill.paymentMode,
newPaymentModeRemoteId = bill.paymentModeRemoteId,
newPaymentModeId = bill.paymentModeId,
newCategoryId = categoryId,
newComment = bill.comment
)
bill.categoryRemoteId = categoryId
bill.categoryId = categoryId
onBillProcessed?.invoke(bill.id)
moveToNext()
}
@@ -87,11 +87,11 @@ class LabelBillsViewModel : ViewModel() {
val start = currentBillIndex
var next = (start + 1) % billsToLabel.size
while (next != start && billsToLabel[next].categoryRemoteId != 0) {
while (next != start && billsToLabel[next].categoryId != 0L) {
next = (next + 1) % billsToLabel.size
}
currentBillIndex = if (billsToLabel[next].categoryRemoteId == 0) {
currentBillIndex = if (billsToLabel[next].categoryId == 0L) {
next
} else {
billsToLabel.size
@@ -33,9 +33,11 @@ class ManageCurrenciesActivity : AppCompatActivity() {
if (message.isEmpty()) {
showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
} else {
viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message),
message=getString(R.string.action_currencies),
positiveText = getString(android.R.string.ok))
viewModel.showDialog(
title = getString(R.string.dialog_sync_error_title),
message = getString(R.string.error_edit_remote_project_helper, message),
positiveText = getString(android.R.string.ok)
)
}
}
override fun onScheduled() {}
@@ -28,6 +28,7 @@ import androidx.core.graphics.toColorInt
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.ColorPicker
import net.helcel.cowspent.android.helper.StatefulAlertDialog
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode
@@ -59,10 +60,10 @@ fun LabelManagementScreenContent(
dialogState: net.helcel.cowspent.android.helper.DialogState?,
onBack: () -> Unit,
onAddCategory: (String, String, String) -> Unit,
onUpdateCategory: (Long, String, String, String) -> Unit,
onUpdateCategory: (DBCategory, String, String, String) -> Unit,
onDeleteCategory: (Long) -> Unit,
onAddPaymentMode: (String, String, String) -> Unit,
onUpdatePaymentMode: (Long, String, String, String) -> Unit,
onUpdatePaymentMode: (DBPaymentMode, String, String, String) -> Unit,
onDeletePaymentMode: (Long) -> Unit,
onDismissDialog: () -> Unit,
onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit,
@@ -123,7 +124,7 @@ fun LabelManagementScreenContent(
if (selectedTab == 0) {
CategoryList(
categories = categories,
categories = categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT },
onEdit = {
editingCategory = it
showEditDialog = true
@@ -162,7 +163,7 @@ fun LabelManagementScreenContent(
if (showEditDialog) {
if (selectedTab == 0) {
EditLabelDialog(
title = if (editingCategory == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit),
title = if (editingCategory == null) stringResource(R.string.title_add_category) else stringResource(R.string.action_edit),
initialName = editingCategory?.name ?: "",
initialIcon = editingCategory?.icon ?: "",
initialColor = editingCategory?.color ?: "#FF0000",
@@ -171,14 +172,14 @@ fun LabelManagementScreenContent(
if (editingCategory == null) {
onAddCategory(name, icon, color)
} else {
onUpdateCategory(editingCategory!!.id, name, icon, color)
onUpdateCategory(editingCategory!!, name, icon, color)
}
showEditDialog = false
}
)
} else {
EditLabelDialog(
title = if (editingPaymentMode == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit),
title = if (editingPaymentMode == null) stringResource(R.string.title_add_payment_mode) else stringResource(R.string.action_edit),
initialName = editingPaymentMode?.name ?: "",
initialIcon = editingPaymentMode?.icon ?: "",
initialColor = editingPaymentMode?.color ?: "#00FF00",
@@ -187,7 +188,7 @@ fun LabelManagementScreenContent(
if (editingPaymentMode == null) {
onAddPaymentMode(name, icon, color)
} else {
onUpdatePaymentMode(editingPaymentMode!!.id, name, icon, color)
onUpdatePaymentMode(editingPaymentMode!!, name, icon, color)
}
showEditDialog = false
}
@@ -353,8 +354,8 @@ fun LabelManagementCategoriesPreview() {
MaterialTheme {
LabelManagementScreenContent(
categories = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", "#FF0000"),
DBCategory(2, 2, 1, "Rent", "🏠", "#00FF00")
DBCategory(1L, 1L, 1L, "Groceries", "🛒", "#FF0000"),
DBCategory(2L, 2L, 1L, "Rent", "🏠", "#00FF00")
),
paymentModes = emptyList(),
dialogState = null,
@@ -379,8 +380,8 @@ fun LabelManagementPaymentModesPreview() {
LabelManagementScreenContent(
categories = emptyList(),
paymentModes = listOf(
DBPaymentMode(1, 1, 1, "Cash", "💵", "#0000FF"),
DBPaymentMode(2, 2, 1, "Credit Card", "💳", "#FFFF00")
DBPaymentMode(1L, 1L, 1L, "Cash", "💵", "#0000FF"),
DBPaymentMode(2L, 2L, 1L, "Credit Card", "💳", "#FFFF00")
),
dialogState = null,
onBack = {},
@@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext
import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
class LabelManagementViewModel(application: Application) : AndroidViewModel(application) {
private val db = CowspentSQLiteOpenHelper.getInstance(application)
@@ -28,44 +28,30 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun loadLabels(projId: Long) {
projectId = projId
viewModelScope.launch {
val cats = withContext(Dispatchers.IO) { db.getCategories(projId) }
val pms = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
if (cats.isEmpty()) {
val defaults = CategoryUtils.getDefaultCategories(getApplication(), projId)
val project = withContext(Dispatchers.IO) { db.getProject(projId) }
if (project != null) {
withContext(Dispatchers.IO) {
defaults.forEach { db.addCategory(it) }
db.ensureDefaultLabels(projId, project.type)
}
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
} else {
categories = cats
}
if (pms.isEmpty()) {
val defaults = CategoryUtils.getDefaultPaymentModes(getApplication(), projId)
withContext(Dispatchers.IO) {
defaults.forEach { db.addPaymentMode(it) }
}
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
} else {
paymentModes = pms
}
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
}
}
fun addCategory(name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.addCategory(DBCategory(0, 0, projectId, name, icon, color))
db.addCategoryAndSync(DBCategory(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
}
loadLabels(projectId)
}
}
fun updateCategory(id: Long, name: String, icon: String, color: String) {
fun updateCategory(cat: DBCategory, name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.updateCategory(id, name, icon, color)
db.updateCategoryAndSync(cat, name, icon, color)
}
loadLabels(projectId)
}
@@ -74,7 +60,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deleteCategory(id: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.deleteCategory(id)
db.deleteCategoryAndSync(id)
}
loadLabels(projectId)
}
@@ -83,16 +69,16 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun addPaymentMode(name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.addPaymentMode(DBPaymentMode(0, 0, projectId, name, icon, color))
db.addPaymentModeAndSync(DBPaymentMode(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
}
loadLabels(projectId)
}
}
fun updatePaymentMode(id: Long, name: String, icon: String, color: String) {
fun updatePaymentMode(pm: DBPaymentMode, name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.updatePaymentMode(id, name, icon, color)
db.updatePaymentModeAndSync(pm, name, icon, color)
}
loadLabels(projectId)
}
@@ -101,7 +87,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deletePaymentMode(id: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.deletePaymentMode(id)
db.deletePaymentModeAndSync(id)
}
loadLabels(projectId)
}
@@ -5,6 +5,7 @@ import android.content.Intent
import net.helcel.cowspent.R
import net.helcel.cowspent.model.*
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.IRefreshBillsListCallback
import java.text.SimpleDateFormat
import java.util.*
@@ -98,6 +99,10 @@ object BillsListUtils {
context: Context
) {
val timestamp = System.currentTimeMillis() / 1000
val proj = db.getProject(projectId)
val categories = db.getCategories(projectId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projectId, proj?.remoteId)
for (t in transactions) {
val owerId = t.owerMemberId
val receiverId = t.receiverMemberId
@@ -106,7 +111,7 @@ object BillsListUtils {
0, 0, projectId, owerId, amount,
timestamp, context.getString(R.string.settle_bill_what),
DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE,
DBBill.PAYMODE_NONE, reimbursementCategoryId,
"", DBBill.PAYMODE_ID_NONE
)
bill.billOwers += DBBillOwer(0, 0, receiverId)
@@ -21,9 +21,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.Sync
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope
@@ -50,6 +48,7 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillFormatter
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.ExportUtil
import net.helcel.cowspent.util.ICallback
@@ -115,18 +114,8 @@ class BillsListViewActivity :
Log.d(TAG, "CREATED project id: $pid")
lifecycleScope.launch {
val addedProj = withContext(Dispatchers.IO) { db.getProject(pid) }
val message: String
val title: String
if (created) {
Log.e(TAG, "CREATED !!!")
title = getString(R.string.msg_project_added)
message = getString(R.string.msg_project_added, addedProj?.remoteId)
} else {
Log.e(TAG, "ADDED !!!")
title = getString(R.string.msg_project_added)
message = getString(R.string.msg_project_added, addedProj?.remoteId)
}
showDialog(message, title, Icons.Default.AddCircleOutline)
val message = getString(R.string.msg_project_added, addedProj?.name?.ifEmpty { addedProj.remoteId } ?: pid.toString())
showToast(this@BillsListViewActivity, message)
}
}
}
@@ -142,7 +131,7 @@ class BillsListViewActivity :
}
if (!db.cowspentServerSyncHelper.isSyncPossible) {
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
Toast.makeText(applicationContext, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show()
showToast(this@BillsListViewActivity, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG)
}
}
}
@@ -565,11 +554,13 @@ class BillsListViewActivity :
val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId)
val balances = HashMap<Long, Double>()
SupportUtil.getStats(
members, bills,
mutableMapOf(), balances, mutableMapOf(), mutableMapOf(),
-1000, -1000, null, null
-1000L, -1000L, reimbursementCategoryId, null, null
)
Triple(proj, members, balances)
}
@@ -636,13 +627,13 @@ class BillsListViewActivity :
mid == null || mid == it.payerId || it.billOwersIds.contains(mid)
}
viewModel.hasUnlabeledBills = bills.any { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED }
viewModel.hasUnlabeledBills = bills.any { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val projectMembers = db.getMembersOfProject(projId, null)
val memberMap = projectMembers.associateBy { it.id }
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.remoteId }
val projectCategories = db.getCategories(projId).associateBy { it.remoteId }
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.id }
val projectCategories = db.getCategories(projId).associateBy { it.id }
BillFormatter.formatBills(
bills,
@@ -708,15 +699,6 @@ class BillsListViewActivity :
}
}
private fun showDialog(msg: String, title: String, icon: ImageVector) {
viewModel.showDialog(
title = title,
message = msg,
positiveText = getString(android.R.string.ok),
icon = icon
)
}
private fun updateUsernameInDrawer() {
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) {
val text = getString(R.string.drawer_no_account)
@@ -118,8 +118,8 @@ object ProjectImportHelper {
val payerWeight = if (columns.containsKey("payer_weight")) line[columns["payer_weight"]!!].toDouble() else 1.0
val owersStr = if (columns.containsKey("owers")) line[columns["owers"]!!] else ""
val payerActive = columns.containsKey("payer_active") && line[columns["payer_active"]!!] == "1"
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toInt() else 0
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toInt() else 0
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toLong() else 0L
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toLong() else 0L
val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null
membersActive[payerName] = payerActive
@@ -149,9 +149,17 @@ object ProjectImportHelper {
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))
paymentModes.forEach { db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color)) }
categories.forEach { db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color)) }
val pmRemoteToLocal = mutableMapOf<Long, Long>()
paymentModes.forEach {
val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color))
pmRemoteToLocal[it.remoteId] = newId
}
val catRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach {
val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color))
catRemoteToLocal[it.remoteId] = newId
}
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
membersWeight.keys.forEach { mName ->
@@ -160,7 +168,9 @@ object ProjectImportHelper {
bills.forEach { b ->
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, b.categoryRemoteId, b.comment, b.paymentModeRemoteId))
val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) }
}
@@ -320,7 +320,6 @@ class NewProjectActivity : AppCompatActivity() {
pid
)
}
showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG)
return pid
}
@@ -71,6 +71,7 @@ import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.CategoryUtils
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -87,6 +88,8 @@ private object SankeyDimens {
@Composable
fun ProjectSankeyDiagram(
projectName: String,
projectId: Long,
projectRemoteId: String?,
allMembers: List<DBMember>,
allBills: List<DBBill>,
customCategories: List<DBCategory>,
@@ -96,12 +99,15 @@ fun ProjectSankeyDiagram(
var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT }
val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) {
CategoryUtils.getReimbursementCategoryId(customCategories, projectId, projectRemoteId)
}
val activeBills = remember(allBills, reimbursementCategoryId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != reimbursementCategoryId }
}
val membersMap = remember(allMembers) { allMembers.associateBy { it.id } }
val categoriesMap = remember(customCategories) { customCategories.associateBy { it.remoteId.toInt() } }
val categoriesMap = remember(customCategories) { customCategories.associateBy { it.id } }
if (activeBills.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -112,13 +118,13 @@ fun ProjectSankeyDiagram(
val spendings = remember(activeBills, selectedMemberId, membersMap) {
val spentMap = mutableMapOf<Long, Double>().apply { membersMap.keys.forEach { put(it, 0.0) } }
val catMap = mutableMapOf<Int, Double>()
val catMap = mutableMapOf<Long, Double>()
activeBills.forEach { bill ->
val totalWeight = bill.billOwers.sumOf { membersMap[it.memberId]?.weight ?: 1.0 }
if (totalWeight > 0) {
if (selectedMemberId == -1L) {
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + bill.amount
catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + bill.amount
bill.billOwers.forEach { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + (bill.amount / totalWeight) * weight
@@ -126,7 +132,7 @@ fun ProjectSankeyDiagram(
} else {
bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + (bill.amount / totalWeight) * weight
catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + (bill.amount / totalWeight) * weight
spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + (bill.amount / totalWeight) * weight
}
}
@@ -215,9 +221,9 @@ private fun SankeyContent(
selectedMemberId: Long,
totalAmount: Double,
displayMemberSpendings: List<Pair<Long, Double>>,
displayCategorySpendings: List<Pair<Int, Double>>,
displayCategorySpendings: List<Pair<Long, Double>>,
membersMap: Map<Long, DBMember>,
categoriesMap: Map<Int, DBCategory>
categoriesMap: Map<Long, DBCategory>
) {
val density = LocalDensity.current
val scope = rememberCoroutineScope()
@@ -399,4 +405,4 @@ private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: F
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", 1L, "1", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
@@ -84,6 +84,21 @@ fun ProjectStatisticsScreen(
val statsData by produceState<StatisticsData?>(null, proj.id) {
value = withContext(Dispatchers.IO) {
val syncedCategories = db.getCategories(proj.id)
val defaultCategories = CategoryUtils.getDefaultCategories(context, proj.id)
val toEnsure = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT })
}
toEnsure.forEach { def ->
if (syncedCategories.none { it.remoteId == def.remoteId }) {
db.addCategory(def)
}
}
val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
@@ -94,19 +109,12 @@ fun ProjectStatisticsScreen(
if (statsData != null) {
val data = statsData!!
val defaultCategories = remember(proj.id) { CategoryUtils.getDefaultCategories(context, proj.id) }
val categories = remember(proj.type, data.categories, defaultCategories) {
val hardcoded = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
(data.categories + hardcoded).distinctBy { it.remoteId }
}
val categories = data.categories
val categoryNoneLabel = stringResource(R.string.category_none)
val sankeyCategories = remember(proj.id, data.categories, defaultCategories, categoryNoneLabel) {
val sankeyCategories = remember(proj.id, data.categories, categoryNoneLabel) {
val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "", "#9E9E9E")
(data.categories + defaultCategories + noneCategory).distinctBy { it.remoteId }
(data.categories + noneCategory).distinctBy { if (it.id == 0L) "none" else it.id.toString() }
}
when (selectedTab) {
@@ -131,6 +139,8 @@ fun ProjectStatisticsScreen(
2 -> {
ProjectSankeyDiagram(
projectName = proj.name.ifEmpty { proj.remoteId },
projectId = proj.id,
projectRemoteId = proj.remoteId,
allMembers = data.members,
allBills = data.bills,
customCategories = sankeyCategories,
@@ -44,8 +44,8 @@ fun ProjectStatisticsTable(
val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) }
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
var categoryId by remember { mutableIntStateOf(-1000) }
var paymentModeId by remember { mutableIntStateOf(-1000) }
var categoryId by remember { mutableLongStateOf(-1000L) }
var paymentModeId by remember { mutableLongStateOf(-1000L) }
var dateMin by remember { mutableStateOf<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 categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) {
val list = mutableListOf<Triple<Int, String, String>>()
list.add(Triple(-1000, "📋", categoryAll))
list.add(Triple(-100, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0, "", categoryNone))
val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000L, "📋", categoryAll))
list.add(Triple(-100L, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0L, "", categoryNone))
val catsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultCategories(context, proj.id)
@@ -75,15 +75,15 @@ fun ProjectStatisticsTable(
}
catsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: ""))
list.add(Triple(it.id, it.icon, it.name ?: ""))
}
list.distinctBy { it.first }
}
val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) {
val list = mutableListOf<Triple<Int, String, String>>()
list.add(Triple(-1000, "💳", paymentModeAll))
list.add(Triple(0, "", paymentModeNone))
val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000L, "💳", paymentModeAll))
list.add(Triple(0L, "", paymentModeNone))
val pmsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultPaymentModes(context, proj.id)
@@ -94,7 +94,7 @@ fun ProjectStatisticsTable(
}
pmsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: ""))
list.add(Triple(it.id, it.icon, it.name ?: ""))
}
list.distinctBy { it.first }
}
@@ -105,10 +105,12 @@ fun ProjectStatisticsTable(
val membersPaid = HashMap<Long, Double>()
val membersSpent = HashMap<Long, Double>()
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(customCategories, proj.id, proj.remoteId)
SupportUtil.getStats(
allMembers, allBills,
membersNbBills, membersBalance, membersPaid, membersSpent,
categoryId, paymentModeId, dateMin, dateMax
categoryId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
)
var statsText = shareStatsIntro + "\n\n"
@@ -16,9 +16,9 @@ open class DBBill(
var state: Int,
var repeat: String?,
var paymentMode: String?,
var categoryRemoteId: Int,
var categoryId: Long,
var comment: String?,
var paymentModeRemoteId: Int
var paymentModeId: Long
) : Item, Serializable {
var formattedWhat: String = ""
@@ -39,7 +39,6 @@ open class DBBill(
get() {
val cal = Calendar.getInstance()
cal.timeInMillis = timestamp * 1000
Log.v("ll", "[$what] get date ts $timestamp year ${cal[Calendar.YEAR]}")
val month = cal[Calendar.MONTH] + 1
val day = cal[Calendar.DAY_OF_MONTH]
return "${cal[Calendar.YEAR]}-${String.format(Locale.ROOT, "%02d", month)}-${
@@ -58,7 +57,7 @@ open class DBBill(
override fun toString(): String {
return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryRemoteId"
return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryId"
}
override fun isSection(): Boolean {
@@ -73,15 +72,15 @@ open class DBBill(
const val PAYMODE_TRANSFER = "t"
const val PAYMODE_ONLINE_SERVICE = "o"
const val PAYMODE_ID_NONE = 0
const val PAYMODE_ID_CARD = -1
const val PAYMODE_ID_CASH = -2
const val PAYMODE_ID_CHECK = -3
const val PAYMODE_ID_TRANSFER = -4
const val PAYMODE_ID_ONLINE_SERVICE = -5
const val PAYMODE_ID_NONE = 0L
const val PAYMODE_ID_CARD = -1L
const val PAYMODE_ID_CASH = -2L
const val PAYMODE_ID_CHECK = -3L
const val PAYMODE_ID_TRANSFER = -4L
const val PAYMODE_ID_ONLINE_SERVICE = -5L
@JvmField
val oldPmIdToNew: Map<String, Int> = object : HashMap<String, Int>() {
val oldPmIdToNew: Map<String, Long> = object : HashMap<String, Long>() {
init {
put(PAYMODE_NONE, PAYMODE_ID_NONE)
put(PAYMODE_CARD, PAYMODE_ID_CARD)
@@ -92,19 +91,19 @@ open class DBBill(
}
}
const val CATEGORY_NONE = 0
const val CATEGORY_GROCERIES = -1
const val CATEGORY_LEISURE = -2
const val CATEGORY_RENT = -3
const val CATEGORY_BILLS = -4
const val CATEGORY_CULTURE = -5
const val CATEGORY_HEALTH = -6
const val CATEGORY_SHOPPING = -10
const val CATEGORY_REIMBURSEMENT = -11
const val CATEGORY_RESTAURANT = -12
const val CATEGORY_ACCOMMODATION = -13
const val CATEGORY_TRANSPORT = -14
const val CATEGORY_SPORT = -15
const val CATEGORY_NONE = 0L
const val CATEGORY_GROCERIES = -1L
const val CATEGORY_LEISURE = -2L
const val CATEGORY_RENT = -3L
const val CATEGORY_BILLS = -4L
const val CATEGORY_CULTURE = -5L
const val CATEGORY_HEALTH = -6L
const val CATEGORY_SHOPPING = -10L
const val CATEGORY_REIMBURSEMENT = -11L
const val CATEGORY_RESTAURANT = -12L
const val CATEGORY_ACCOMMODATION = -13L
const val CATEGORY_TRANSPORT = -14L
const val CATEGORY_SPORT = -15L
const val STATE_OK = 0
const val STATE_ADDED = 1
@@ -15,9 +15,9 @@ class GroupedBill(
sourceBills.first().state,
sourceBills.first().repeat,
sourceBills.first().paymentMode,
sourceBills.first().categoryRemoteId,
sourceBills.first().categoryId,
sourceBills.first().comment,
sourceBills.first().paymentModeRemoteId
sourceBills.first().paymentModeId
), Serializable {
init {
this.formattedWhat = sourceBills.first().formattedWhat
@@ -13,9 +13,9 @@ import androidx.preference.PreferenceManager
import net.helcel.cowspent.R
import net.helcel.cowspent.android.main.BillsListViewActivity
import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SecureStorage
import net.helcel.cowspent.util.SupportUtil
import java.text.SimpleDateFormat
import java.util.*
/**
@@ -156,10 +156,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
@SuppressLint("Range")
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 1) {
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
recreateDatabase(db)
@@ -202,311 +199,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.execSQL("CREATE INDEX IF NOT EXISTS $indexName ON $table($key_id)")
}
fun addAccountProject(accountProject: DBAccountProject): Long {
val db = writableDatabase
val values = ContentValues()
values.put(key_remoteId, accountProject.remoteId)
values.put(key_ncUrl, accountProject.ncUrl)
values.put(key_name, accountProject.name)
values.put(key_archived, accountProject.archivedTs ?: 0L)
val id = db.insert(table_account_projects, null, values)
SecureStorage.savePasswordSync(context, "AccountProjectPassword_$id", accountProject.password)
return id
}
val accountProjects: List<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()))
}
// --- Projects logic ---
fun addProject(project: DBProject): Long {
val db = writableDatabase
@@ -571,16 +264,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
)
}
fun deleteProject(id: Long) {
val db = writableDatabase
for (b in getBillsOfProject(id)) {
deleteBill(b.id)
}
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
}
fun updateProject(
projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?,
@@ -647,8 +330,19 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
}
fun deleteProject(id: Long) {
val db = writableDatabase
for (b in getBillsOfProject(id)) {
deleteBill(b.id)
}
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
}
// --- Members logic ---
fun addMember(m: DBMember): Long {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add member]") }
val db = writableDatabase
val values = ContentValues()
values.put(key_remoteId, m.remoteId)
@@ -705,7 +399,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
m.id, newName, newWeight, newActivated, newState, null,
newR, newG, newB, newNcUserId, newAvatar
)
Log.v(TAG, "UPDATE MEMBER AND SYNC")
val proj = getProject(m.projectId)
if (proj != null) syncIfRemote(proj)
}
@@ -780,8 +473,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_members, "$key_id = ?", arrayOf(id.toString()))
}
// --- Bills logic ---
fun addBill(b: DBBill): Long {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add bill]") }
val db = writableDatabase
val values = ContentValues()
values.put(key_remoteId, b.remoteId)
@@ -866,7 +560,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
}
}
Log.v(TAG, "UPDATE BILL AND SYNC")
val proj = getProject(bill.projectId)
if (proj != null) syncIfRemote(proj)
}
@@ -887,29 +580,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
return getBillsCustom("$key_payer_id = ?", arrayOf(memberId.toString()), "$key_timestamp ASC")
}
@Suppress("unused")
fun getBill(remoteId: Long, projId: Long): DBBill? {
val bills = getBillsCustom(
"$key_remoteId = ? AND $key_projectid = ?",
arrayOf(remoteId.toString(), projId.toString()),
null
)
fun getBill(id: Long): DBBill? {
val bills = getBillsCustom("$key_id = ?", arrayOf(id.toString()), null)
return if (bills.isEmpty()) null else bills[0]
}
fun getBill(billId: Long): DBBill? {
val bills = getBillsCustom("$key_id = ?", arrayOf(billId.toString()), null)
return if (bills.isEmpty()) null else bills[0]
}
fun getCurrenciesOfProjectWithState(projId: Long, state: Int): List<DBCurrency> {
return getCurrenciesCustom(
"$key_projectid = ? AND $key_state = ?",
arrayOf(projId.toString(), state.toString()),
null
)
}
@WorkerThread
fun searchBills(query: CharSequence?, projectId: Long): List<DBBill> {
val andWhere: MutableList<String> = ArrayList()
@@ -1019,8 +694,9 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_bills, "$key_id = ?", arrayOf(id.toString()))
}
// --- Billowers logic ---
fun addBillower(billId: Long, memberId: Long) {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add billower]") }
val db = writableDatabase
val values = ContentValues()
values.put(key_billId, billId)
@@ -1062,6 +738,443 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_billowers, "$key_id = ?", arrayOf(id.toString()))
}
// --- Categories logic ---
fun addCategory(category: DBCategory): Long {
val db = writableDatabase
val values = ContentValues()
values.put(key_remoteId, category.remoteId)
values.put(key_projectid, category.projectId)
values.put(key_name, category.name)
values.put(key_icon, category.icon)
values.put(key_color, category.color)
values.put(key_state, category.state)
return db.insert(table_categories, null, values)
}
fun addCategoryAndSync(cat: DBCategory) {
addCategory(cat)
val proj = getProject(cat.projectId)
if (proj != null) syncIfRemote(proj)
}
fun getCategory(remoteId: Long, projectId: Long): DBCategory? {
val categories = getCategoriesCustom(
"$key_remoteId = ? AND $key_projectid = ?",
arrayOf(remoteId.toString(), projectId.toString()),
null
)
return if (categories.isEmpty()) null else categories[0]
}
fun getCategory(id: Long): DBCategory? {
val categories = getCategoriesCustom("$key_id = ?", arrayOf(id.toString()), null)
return if (categories.isEmpty()) null else categories[0]
}
fun getCategories(projectId: Long, includeDeleted: Boolean = false): List<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")
companion object {
private val TAG = CowspentSQLiteOpenHelper::class.java.simpleName
@@ -1100,16 +1213,16 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
private const val key_repeat = "REPEAT"
private const val key_payment_mode = "PAYMENTMODE"
private const val key_payment_mode_id = "PAYMENTMODEID"
private const val key_category_id = "CATEGORYID"
private const val key_comment = "COMMENT"
const val key_category_id = "CATEGORYID"
const val key_comment = "COMMENT"
private const val table_billowers = "BILLOWERS"
private const val key_billId = "BILLID"
private const val key_member_id = "MEMBERID"
private const val table_account_projects = "ACCOUNTPROJECTS"
private const val key_ncUrl = "NCURL"
private const val table_categories = "CATEGORIES"
private const val key_icon = "ICON"
private const val key_color = "COLOR"
const val key_icon = "ICON"
const val key_color = "COLOR"
private const val table_payment_modes = "PAYMENTMODES"
private const val key_latest_bill_ts = "LATEST_BILL_TS"
private const val table_currencies = "CURRENCIES"
@@ -29,9 +29,11 @@ import net.helcel.cowspent.android.account.AccountActivity
import net.helcel.cowspent.android.main.BillsListViewActivity
import net.helcel.cowspent.android.main.MainConstants
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.ICallback
import net.helcel.cowspent.util.IProjectCreationCallback
import net.helcel.cowspent.util.NextcloudClient
@@ -265,6 +267,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val members = dbHelper.getMembersOfProject(project.id, null)
val memberIdToRemoteId = members.associate { it.id to it.remoteId }
val categoriesToAdd = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (catToAdd in categoriesToAdd) {
try {
val categoriesResponse = client!!.getCategories(project)
val remoteCategories = categoriesResponse.getCategories(project.id)
val matchingRemote = remoteCategories.find { it.name == catToAdd.name }
if (matchingRemote != null) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, matchingRemote.remoteId)
catToAdd.remoteId = matchingRemote.remoteId
} else {
val createResponse = client!!.createRemoteCategory(project, catToAdd)
val newRemoteId = createResponse.remoteCategoryId
if (newRemoteId > 0) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
catToAdd.remoteId = newRemoteId
}
}
} catch (e: Exception) {
Log.e(TAG, "CATEGORY SYNC FAILED for ${catToAdd.name}", e)
}
}
val categoriesToEdit = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (catToEdit in categoriesToEdit) {
try {
client!!.editRemoteCategory(project, catToEdit)
dbHelper.updateCategory(catToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT CATEGORY FAILED for ${catToEdit.name}, might not exist remotely", e)
// If it fails, we keep the state as EDITED so it tries again next time,
// or we could set it to OK if we think it's a permanent mismatch.
// For now just log it.
}
}
val categoriesToDelete = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (catToDel in categoriesToDelete) {
try {
client!!.deleteRemoteCategory(project, catToDel.remoteId)
dbHelper.deleteCategory(catToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete category on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deleteCategory(catToDel.id)
} else {
throw e
}
} catch (e: Exception) {
Log.e(TAG, "DELETE CATEGORY FAILED for ${catToDel.name}", e)
}
}
val paymentModesToAdd = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_ADDED)
if (paymentModesToAdd.isNotEmpty()) {
try {
val pmsResponse = client!!.getPaymentModes(project)
val remotePms = pmsResponse.getPaymentModes(project.id)
val remotePmsNames = remotePms.map { it.name }
for (pmToAdd in paymentModesToAdd) {
val searchIndex = remotePmsNames.indexOf(pmToAdd.name)
if (searchIndex != -1) {
val remotePm = remotePms[searchIndex]
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, remotePm.remoteId)
pmToAdd.remoteId = remotePm.remoteId
} else {
val createRemotePaymentModeResponse = client!!.createRemotePaymentMode(project, pmToAdd)
val newRemoteId = createRemotePaymentModeResponse.remotePaymentModeId
if (newRemoteId > 0) {
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
pmToAdd.remoteId = newRemoteId
}
}
}
} catch (e: NextcloudHttpRequestFailedException) {
Log.e(TAG, "GET PAYMENT MODES FAILED : " + e.message)
}
}
val paymentModesToEdit = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (pmToEdit in paymentModesToEdit) {
try {
client!!.editRemotePaymentMode(project, pmToEdit)
dbHelper.updatePaymentMode(pmToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT PAYMENT MODE FAILED for ${pmToEdit.name}, might not exist remotely", e)
}
}
val paymentModesToDelete = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (pmToDel in paymentModesToDelete) {
try {
client!!.deleteRemotePaymentMode(project, pmToDel.remoteId)
dbHelper.deletePaymentMode(pmToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete payment mode on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deletePaymentMode(pmToDel.id)
} else {
throw e
}
} catch (_: Exception) {
}
}
val categories = dbHelper.getCategories(project.id)
val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }.toMutableMap()
// Map hardcoded constants to themselves if not in DB
categoryIdToRemoteId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
categories.filter { it.remoteId < 0 }.forEach { categoryIdToRemoteId[it.remoteId] = it.remoteId }
val paymentModes = dbHelper.getPaymentModes(project.id)
val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }.toMutableMap()
paymentModes.filter { it.remoteId < 0 }.forEach { paymentModeIdToRemoteId[it.remoteId] = it.remoteId }
val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (bToDel in toDelete) {
try {
@@ -293,12 +410,16 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toEdit = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (bToEdit in toEdit) {
try {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId)
if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val returnedRemoteId = editRemoteBillResponse.remoteBillId
if (returnedRemoteId == bToEdit.remoteId || (returnedRemoteId == 0L && !project.getRequestBaseUrl(true).contains("/ocs/v2.php"))) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else if (returnedRemoteId > 0) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else {
Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)")
}
} catch (_: Exception) {
Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely")
@@ -307,8 +428,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (bToAdd in toAdd) {
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId)
val newRemoteId = createRemoteBillResponse.stringContent.toLong()
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val newRemoteId = createRemoteBillResponse.remoteBillId
if (newRemoteId > 0) {
dbHelper.updateBill(
bToAdd.id, newRemoteId, null,
@@ -375,7 +496,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong()
val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId
if (newRemoteId > 0) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
}
@@ -471,7 +592,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localPaymentModes = dbHelper.getPaymentModes(project.id)
for (localPaymentMode in localPaymentModes) {
if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
dbHelper.deletePaymentMode(localPaymentMode.id)
Log.d(TAG, "Delete local pm : $localPaymentMode")
}
@@ -481,6 +602,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId }
for (c in remoteCategories) {
if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue
val localCategory = dbHelper.getCategory(c.remoteId, project.id)
if (localCategory == null) {
Log.d(TAG, "Add local category : $c")
@@ -500,7 +622,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCategories = dbHelper.getCategories(project.id)
for (localCategory in localCategories) {
if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
dbHelper.deleteCategory(localCategory.id)
Log.d(TAG, "Delete local category : $localCategory")
}
@@ -528,7 +650,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCurrencies = dbHelper.getCurrencies(project.id)
for (localCurrency in localCurrencies) {
if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
dbHelper.deleteCurrency(localCurrency.id)
Log.d(TAG, "Delete local currency : $localCurrencies")
}
@@ -595,13 +717,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val dbMembers = dbHelper.getMembersOfProject(project.id, null)
val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id }
val dbCategories = dbHelper.getCategories(project.id)
val categoriesRemoteIdToId = dbCategories.associate { it.remoteId to it.id }.toMutableMap()
// Map hardcoded constants to their local IDs if they exist in DB, else to themselves
categoriesRemoteIdToId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
dbCategories.filter { it.remoteId < 0 }.forEach { categoriesRemoteIdToId[it.remoteId] = it.id }
val dbPaymentModes = dbHelper.getPaymentModes(project.id)
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
val billsResponse = client!!.getBills(project)
val isIHM = project.type == ProjectType.IHATEMONEY
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
val remoteBills: List<DBBill> = if (isIHM) {
billsResponse.getBillsIHM(project.id, memberRemoteIdToId)
billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} else {
billsResponse.getBillsCospend(project.id, memberRemoteIdToId)
billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
}
val remoteAllBillIds: List<Long> = if (isIHM) {
remoteBills.map { it.remoteId }
@@ -618,7 +750,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
dbHelper.addBill(remoteBill)
nbPulledNewBills++
newBillsDialogText += "+ ${remoteBill.what}\n"
Log.d(TAG, "Add local bill : $remoteBill")
} else {
val localBill = localBillsByRemoteId[remoteBill.remoteId]!!
if (hasChanged(localBill, remoteBill)) {
@@ -626,12 +757,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.id, null, remoteBill.payerId,
remoteBill.amount, remoteBill.timestamp,
remoteBill.what, DBBill.STATE_OK, remoteBill.repeat,
remoteBill.paymentMode, remoteBill.paymentModeRemoteId,
remoteBill.categoryRemoteId, remoteBill.comment
remoteBill.paymentMode, remoteBill.paymentModeId,
remoteBill.categoryId, remoteBill.comment
)
nbPulledUpdatedBills++
updatedBillsDialogText += "${remoteBill.what}\n"
Log.d(TAG, "Update local bill : $remoteBill")
} else {
Log.d(TAG, "Nothing to do for bill : $localBill")
}
@@ -1084,8 +1214,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.what == remoteBill.what &&
localBill.comment == remoteBill.comment &&
localBill.paymentMode == remoteBill.paymentMode &&
localBill.paymentModeRemoteId == remoteBill.paymentModeRemoteId &&
localBill.categoryRemoteId == remoteBill.categoryRemoteId
localBill.paymentModeId == remoteBill.paymentModeId &&
localBill.categoryId == remoteBill.categoryId
) {
val localRepeat = localBill.repeat ?: DBBill.NON_REPEATED
val remoteRepeat = remoteBill.repeat ?: DBBill.NON_REPEATED
@@ -14,11 +14,11 @@ object BillFormatter {
) {
for (bill in bills) {
var whatPrefix = ""
val pm = paymentModesMap[bill.paymentModeRemoteId.toLong()]
val pm = paymentModesMap[bill.paymentModeId]
if (pm != null) {
whatPrefix += pm.icon + " "
} else {
when (bill.paymentModeRemoteId) {
when (bill.paymentModeId) {
DBBill.PAYMODE_ID_CARD -> whatPrefix += "\uD83D\uDCB3 "
DBBill.PAYMODE_ID_CASH -> whatPrefix += "\uD83D\uDCB5 "
DBBill.PAYMODE_ID_CHECK -> whatPrefix += "\uD83C\uDFAB "
@@ -27,11 +27,11 @@ object BillFormatter {
}
}
val cat = categoriesMap[bill.categoryRemoteId.toLong()]
val cat = categoriesMap[bill.categoryId]
if (cat != null) {
whatPrefix += cat.icon + " "
} else {
when (bill.categoryRemoteId) {
when (bill.categoryId) {
DBBill.CATEGORY_GROCERIES -> whatPrefix += "\uD83D\uDED2 "
DBBill.CATEGORY_LEISURE -> whatPrefix += "\uD83C\uDF89 "
DBBill.CATEGORY_RENT -> whatPrefix += "\uD83C\uDFE0 "
@@ -10,28 +10,57 @@ object CategoryUtils {
fun getDefaultCategories(context: Context, projectId: Long): List<DBCategory> {
return listOf(
DBCategory(0, DBBill.CATEGORY_GROCERIES.toLong(), projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"),
DBCategory(0, DBBill.CATEGORY_LEISURE.toLong(), projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"),
DBCategory(0, DBBill.CATEGORY_RENT.toLong(), projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"),
DBCategory(0, DBBill.CATEGORY_BILLS.toLong(), projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"),
DBCategory(0, DBBill.CATEGORY_CULTURE.toLong(), projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"),
DBCategory(0, DBBill.CATEGORY_HEALTH.toLong(), projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"),
DBCategory(0, DBBill.CATEGORY_SHOPPING.toLong(), projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"),
DBCategory(0, DBBill.CATEGORY_REIMBURSEMENT.toLong(), projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"),
DBCategory(0, DBBill.CATEGORY_RESTAURANT.toLong(), projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"),
DBCategory(0, DBBill.CATEGORY_ACCOMMODATION.toLong(), projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"),
DBCategory(0, DBBill.CATEGORY_TRANSPORT.toLong(), projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"),
DBCategory(0, DBBill.CATEGORY_SPORT.toLong(), projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177")
DBCategory(DBBill.CATEGORY_GROCERIES,
DBBill.CATEGORY_GROCERIES, projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"),
DBCategory(DBBill.CATEGORY_LEISURE,
DBBill.CATEGORY_LEISURE, projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"),
DBCategory(DBBill.CATEGORY_RENT,
DBBill.CATEGORY_RENT, projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"),
DBCategory(DBBill.CATEGORY_BILLS,
DBBill.CATEGORY_BILLS, projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"),
DBCategory(DBBill.CATEGORY_CULTURE,
DBBill.CATEGORY_CULTURE, projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"),
DBCategory(DBBill.CATEGORY_HEALTH,
DBBill.CATEGORY_HEALTH, projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"),
DBCategory(DBBill.CATEGORY_SHOPPING,
DBBill.CATEGORY_SHOPPING, projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"),
DBCategory(DBBill.CATEGORY_REIMBURSEMENT,
DBBill.CATEGORY_REIMBURSEMENT, projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"),
DBCategory(DBBill.CATEGORY_RESTAURANT,
DBBill.CATEGORY_RESTAURANT, projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"),
DBCategory(DBBill.CATEGORY_ACCOMMODATION,
DBBill.CATEGORY_ACCOMMODATION, projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"),
DBCategory(DBBill.CATEGORY_TRANSPORT,
DBBill.CATEGORY_TRANSPORT, projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"),
DBCategory(DBBill.CATEGORY_SPORT,
DBBill.CATEGORY_SPORT, projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177")
)
}
fun getDefaultPaymentModes(context: Context, projectId: Long): List<DBPaymentMode> {
return listOf(
DBPaymentMode(0, DBBill.PAYMODE_ID_CARD.toLong(), projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CASH.toLong(), projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CHECK.toLong(), projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"),
DBPaymentMode(0, DBBill.PAYMODE_ID_TRANSFER.toLong(), projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"),
DBPaymentMode(0, DBBill.PAYMODE_ID_ONLINE_SERVICE.toLong(), projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc")
DBPaymentMode(DBBill.PAYMODE_ID_CARD, DBBill.PAYMODE_ID_CARD, projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"),
DBPaymentMode(DBBill.PAYMODE_ID_CASH, DBBill.PAYMODE_ID_CASH, projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"),
DBPaymentMode(DBBill.PAYMODE_ID_CHECK, DBBill.PAYMODE_ID_CHECK, projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"),
DBPaymentMode(DBBill.PAYMODE_ID_TRANSFER, DBBill.PAYMODE_ID_TRANSFER, projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"),
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 }
}
}
@@ -48,7 +48,7 @@ object ExportUtil {
}
owersTxt = owersTxt.replace(",$".toRegex(), "")
fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," +
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryRemoteId}," +
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryId}," +
"${b.paymentMode}\n"
}
@@ -93,23 +93,30 @@ class NextcloudClient(
params: Collection<QueryParam>?,
isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val headers: MutableMap<String, List<String>> = HashMap()
if (isOCSRequest) {
val acceptHeader: MutableList<String> = ArrayList()
acceptHeader.add("application/json")
headers["Accept"] = acceptHeader
val ocsHeader: MutableList<String> = ArrayList()
ocsHeader.add("true")
headers["OCS-APIRequest"] = ocsHeader
}
val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setHeader(headers)
.build()
} else {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setParameter(params)
.setHeader(headers)
.build()
@@ -182,8 +189,12 @@ class NextcloudClient(
target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val targetURL = url + target.replace("^/".toRegex(), "")
val targetURL = url + finalTarget.replace("^/".toRegex(), "")
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
val httpCon = SupportUtil.getHttpURLConnection(targetURL)
httpCon.requestMethod = method
@@ -194,7 +205,7 @@ class NextcloudClient(
)
}
httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "cowspent-android/" + SupportUtil.getAppVersionName(context))
httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag)
}
@@ -244,8 +255,12 @@ class NextcloudClient(
target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
var strBase64: String
val targetURL = url + target.replace("^/".toRegex(), "")
val targetURL = url + finalTarget.replace("^/".toRegex(), "")
val httpCon = SupportUtil.getHttpURLConnection( targetURL)
httpCon.requestMethod = method
if (needLogin) {
@@ -68,7 +68,7 @@ open class ServerResponse(
}
val rawData = JSONObject(content)
val data = rawData.getJSONObject("ocs")
return data.getString("data")
return data.get("data").toString()
}
class ProjectResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) :
@@ -137,6 +137,90 @@ open class ServerResponse(
getResponseStringData().toLong()
}
class CreateRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCategoryId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remotePaymentModeId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemoteCurrencyResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
@@ -145,6 +229,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCurrencyId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemoteCurrencyResponse(
@@ -196,6 +292,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class CreateRemoteBillResponse(
@@ -206,6 +314,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class DeleteRemoteBillResponse(
@@ -242,13 +362,23 @@ open class ServerResponse(
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getBillsCospend(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> {
return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId)
fun getBillsCospend(
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)
fun getBillsIHM(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> {
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId)
fun getBillsIHM(
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(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
@@ -366,6 +522,26 @@ open class ServerResponse(
return members
}
@Throws(JSONException::class)
protected fun getCategoriesFromJSONArray(jsonCats: JSONArray, projId: Long): List<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)
protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList()
@@ -382,6 +558,27 @@ open class ServerResponse(
return categories
}
@Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, projId: Long): DBCategory {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBCategory(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory {
val remoteId = remoteIdStr.toLong()
@@ -416,6 +613,27 @@ open class ServerResponse(
return paymentModes
}
@Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, projId: Long): DBPaymentMode {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBPaymentMode(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode {
val remoteId = remoteIdStr.toLong()
@@ -556,26 +774,30 @@ open class ServerResponse(
protected fun getBillsFromJSONArray(
json: JSONArray,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
val bills: MutableList<DBBill> = ArrayList()
for (i in 0 until json.length()) {
val jsonBill = json.getJSONObject(i)
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId))
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId))
}
return bills
}
@Throws(JSONException::class)
protected fun getBillsFromJSONObject(
json: JSONObject,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
val bills: List<DBBill>
val json = getResponseObjectData()
if (json.has("bills") && !json.isNull("bills")) {
val jsonBills = json.getJSONArray("bills")
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId)
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} else {
bills = ArrayList()
}
@@ -586,7 +808,9 @@ open class ServerResponse(
protected fun getBillFromJSON(
json: JSONObject,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): DBBill {
var remoteId: Long = 0
var payerRemoteId: Long
@@ -600,7 +824,7 @@ open class ServerResponse(
var repeat = DBBill.NON_REPEATED
var paymentMode = DBBill.PAYMODE_NONE
var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE
var categoryId = DBBill.CATEGORY_NONE
var categoryRemoteId = DBBill.CATEGORY_NONE
if (!json.isNull("id")) {
remoteId = json.getLong("id")
}
@@ -639,23 +863,29 @@ open class ServerResponse(
}
if (json.has("paymentmode") && !json.isNull("paymentmode")) {
paymentMode = json.getString("paymentmode")
} else if (json.has("paymentMode") && !json.isNull("paymentMode")) {
paymentMode = json.getString("paymentMode")
}
if (json.has("categoryid") && !json.isNull("categoryid")) {
categoryId = json.getInt("categoryid")
Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId")
categoryRemoteId = json.getLong("categoryid")
} else if (json.has("categoryId") && !json.isNull("categoryId")) {
categoryRemoteId = json.getLong("categoryId")
}
if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) {
paymentModeRemoteId = json.getInt("paymentmodeid")
paymentModeRemoteId = json.getLong("paymentmodeid")
} else if (json.has("paymentModeId") && !json.isNull("paymentModeId")) {
paymentModeRemoteId = json.getLong("paymentModeId")
}
// old MB, new Cospend is ok as Cospend provides the old pm ID
// new MB, old Cospend => set payment mode ID from old one
if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) {
Log.d("PaymentMode", "old: $paymentMode and new: 0")
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
}
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
val bill = DBBill(
0, remoteId, projId, payerId, amount, timestamp, what,
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeRemoteId
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId
)
bill.billOwers = getBillOwersFromJson(json, memberRemoteIdToId)
return bill
@@ -87,14 +87,17 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int,
catId: Long, paymentModeId: Long,
dateMin: String?, dateMax: String?
): Int {
val proj = db.getProject(projId)
val categories = db.getCategories(projId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projId, proj?.remoteId)
return getStats(
db.getMembersOfProject(projId, null),
db.getBillsOfProject(projId),
membersNbBills, membersBalance, membersPaid, membersSpent,
catId, paymentModeId, dateMin, dateMax
catId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
)
}
@@ -106,7 +109,8 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int,
catId: Long, paymentModeId: Long,
reimbursementCategoryId: Long,
dateMin: String?, dateMax: String?
): Int {
val nbBillsTotal = 0
@@ -124,9 +128,9 @@ object SupportUtil {
for (b in dbBills) {
// don't take deleted bills and respect category filter
if (b.state != DBBill.STATE_DELETED &&
((catId == -1000 || catId == -100 || b.categoryRemoteId == catId) &&
(catId != -100 || b.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT) &&
(paymentModeId == -1000 || b.paymentModeRemoteId == paymentModeId)) &&
((catId == -1000L || catId == -100L || b.categoryId == catId) &&
(catId != -100L || b.categoryId != reimbursementCategoryId) &&
(paymentModeId == -1000L || b.paymentModeId == paymentModeId)) &&
(dateMin == null || b.date >= dateMin) &&
(dateMax == null || b.date <= dateMax)
) {
@@ -285,18 +289,6 @@ object SupportUtil {
return reduceBalance(crediters, debiters, results)
}
@JvmStatic
fun getVersionName(context: Context): String {
var versionName = "0.0.0"
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
versionName = pInfo.versionName ?: "0.0.0"
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return versionName
}
@JvmStatic
fun getJsonObject(text: String?): JSONObject? {
if (text == null) return null
@@ -11,8 +11,10 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
import com.nextcloud.android.sso.exceptions.TokenMismatchException
import com.nextcloud.android.sso.model.SingleSignOnAccount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
import org.json.JSONException
@@ -82,13 +84,11 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false)
}
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId
return ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
@@ -260,7 +260,13 @@ class VersatileProjectSyncClient(
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.EditRemoteBillResponse {
fun editRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.EditRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date")
@@ -307,8 +313,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString())
paramValues.add(bill.paymentModeRemoteId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -512,7 +518,13 @@ class VersatileProjectSyncClient(
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.CreateRemoteBillResponse {
fun createRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.CreateRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date")
@@ -554,8 +566,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString())
paramValues.add(bill.paymentModeRemoteId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -634,7 +646,6 @@ class VersatileProjectSyncClient(
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
Log.i(TAG, "using new API for createRemoteBill")
ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members"
@@ -698,7 +709,6 @@ class VersatileProjectSyncClient(
paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
Log.i(TAG, "using new API for getBills")
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills"
@@ -733,6 +743,92 @@ class VersatileProjectSyncClient(
}
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getCategories(project: DBProject): ServerResponse.CategoriesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CategoriesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CategoriesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getPaymentModes(project: DBProject): ServerResponse.PaymentModesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.PaymentModesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.PaymentModesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getMembers(project: DBProject): ServerResponse.MembersResponse {
var target: String
@@ -779,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)
fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse {
val paramKeys: MutableList<String> = ArrayList()
@@ -936,6 +1331,10 @@ class VersatileProjectSyncClient(
nextcloudAPI: NextcloudAPI, target: String, method: String,
paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean
): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
var params: MutableList<QueryParam>? = null
if (paramKeys != null && paramValues != null) {
@@ -949,17 +1348,20 @@ class VersatileProjectSyncClient(
val acceptHeader: MutableList<String> = ArrayList()
acceptHeader.add("application/json")
headers["Accept"] = acceptHeader
val ocsHeader: MutableList<String> = ArrayList()
ocsHeader.add("true")
headers["OCS-APIRequest"] = ocsHeader
}
val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setHeader(headers)
.build()
} else {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setParameter(params)
.setHeader(headers)
.build()
@@ -993,8 +1395,12 @@ class VersatileProjectSyncClient(
lastETag: String?, username: String?, password: String?,
bearerToken: String?, isOCSRequest: Boolean
): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val httpCon = SupportUtil.getHttpURLConnection(target)
val httpCon = SupportUtil.getHttpURLConnection(finalTarget)
httpCon.requestMethod = method
if (bearerToken != null) {
httpCon.setRequestProperty("Authorization", "Bearer $bearerToken")
@@ -1005,7 +1411,7 @@ class VersatileProjectSyncClient(
)
}
httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "Cowspent/" + SupportUtil.getAppVersionName(context))
httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag)
}
+3 -1
View File
@@ -35,6 +35,8 @@
<string name="title_settle">Settle Project</string>
<string name="title_share">Share 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_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
@@ -229,7 +231,7 @@
<string name="settle_bill_what">Settlement</string>
<!-- 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_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
-2
View File
@@ -19,8 +19,6 @@ This means you can choose where your data is going and preserve your privacy.
* Dark theme and customizable main app color
* Share/import projects with link/QRCode
* Connect to a Nextcloud account to automatically add projects
* Background sync service with notifications on bills events
* Multi-lingual user-interface (translated on Crowdin: https://crowdin.com/project/cowspent)
# Requirements