6 Commits

Author SHA1 Message Date
sora 553e33d441 Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2026-07-12 14:47:56 +02:00
sora eec95cdcb0 Merge branch 'main' into renovate/configure 2026-07-12 14:47:47 +02:00
soraefir 106c4e27fc Fix sync and add beta features 2026-07-09 19:22:04 +02:00
soraefir 47d0471b48 Label sync and workflows 2026-07-05 15:02:36 +02:00
sora 56968dbe3b Update metadata/en-US/full_description.txt 2026-07-02 03:05:16 +02:00
Renovate Bot c2f59fa1df Add renovate.json 2026-05-30 02:02:13 +00:00
30 changed files with 1488 additions and 584 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ android {
applicationId "net.helcel.cowspent"
minSdk = 26
targetSdk = 37
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1"
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
}
@@ -26,7 +26,6 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil
import java.text.ParseException
import java.time.ZoneId
@@ -52,23 +51,10 @@ class EditBillActivity : AppCompatActivity() {
ThemeUtils.CowspentTheme {
val categories = remember {
val syncedCategories = db.getCategories(bill.projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@EditBillActivity, bill.projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT.toLong() })
}
syncedCategories + hardcoded
db.getCategories(bill.projectId)
}
val paymentModes = remember {
val syncedPaymentModes = db.getPaymentModes(bill.projectId)
val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId)
if (projectType == ProjectType.LOCAL) {
syncedPaymentModes + defaultPaymentModes
} else {
syncedPaymentModes.ifEmpty { defaultPaymentModes }
}
db.getPaymentModes(bill.projectId)
}
EditBillScreen(
@@ -168,7 +154,7 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
0, 0, projectId, 0, 0.0, timeNowSeconds,
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE.toLong(), "", DBBill.PAYMODE_ID_NONE.toLong()
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE
)
} else {
val btd = db.getBill(billIdToDuplicate)!!
@@ -195,6 +181,11 @@ class EditBillActivity : AppCompatActivity() {
val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId)
if (project != null) {
db.ensureDefaultLabels(bill.projectId, project.type)
}
withContext(Dispatchers.Main) {
viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: ""
@@ -399,6 +390,8 @@ class EditBillActivity : AppCompatActivity() {
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
)
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
newBill.categoryId = viewModel.categoryId
newBill.paymentModeId = viewModel.paymentModeId
val newId = db.addBill(newBill)
if (firstSavedId == 0L) firstSavedId = newId
}
@@ -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 = LocalContext.current
var categoryExpanded by remember { mutableStateOf(false) }
val selectedCategory =
categories.find { (if (it.id > 0) it.id else it.remoteId) == viewModel.categoryId }
categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId)
EditableExposedDropdownMenu(
value = selectedCategory?.name ?: "",
@@ -495,9 +497,19 @@ fun BillAdditionalDetailsSection(
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_none))
}
categories.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT
categoryExpanded = false
}) {
Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_reimbursement))
}
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryId = if (category.id > 0) category.id else category.remoteId
viewModel.categoryId = category.id
categoryExpanded = false
}) {
Text(text = category.icon, fontSize = 20.sp)
@@ -512,7 +524,7 @@ fun BillAdditionalDetailsSection(
var pmExpanded by remember { mutableStateOf(false) }
val selectedPm =
paymentModes.find { (if (it.id > 0) it.id else it.remoteId) == viewModel.paymentModeId }
paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId)
EditableExposedDropdownMenu(
value = selectedPm?.name ?: "",
@@ -541,7 +553,7 @@ fun BillAdditionalDetailsSection(
}
paymentModes.forEach { pm ->
DropdownMenuItem(onClick = {
viewModel.paymentModeId = if (pm.id > 0) pm.id else pm.remoteId
viewModel.paymentModeId = pm.id
pmExpanded = false
}) {
Text(text = pm.icon, fontSize = 20.sp)
@@ -2,22 +2,20 @@ package net.helcel.cowspent.android.bill_edit
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath
import net.helcel.cowspent.model.DBCurrency
import androidx.compose.ui.graphics.vector.ImageVector
class EditBillViewModel : ViewModel() {
var what by mutableStateOf("")
var amount by mutableStateOf("")
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType
@@ -42,14 +41,10 @@ class LabelBillsActivity : AppCompatActivity() {
val billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryId != 0L && it.state != DBBill.STATE_DELETED }
val syncedCategories = db.getCategories(projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT.toLong() })
}
val categories = syncedCategories + hardcoded
db.ensureDefaultLabels(projectId, projectType)
// Reload from DB to get real IDs
val categories = db.getCategories(projectId)
Quadruple(members, billsToLabel, categories, allCategorized)
}
@@ -248,10 +248,10 @@ fun LabelBillsScreenPreview() {
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L)
)
val cats = listOf(
DBCategory(-1, -1, 1, "Groceries", "🛒", ""),
DBCategory(-2, -2, 1, "Leisure", "🥳", ""),
DBCategory(-3, -3, 1, "Rent", "🏠", ""),
DBCategory(-4, -4, 1, "Bills", "💸", "")
DBCategory(1L, 1L, 1L, "Groceries", "🛒", ""),
DBCategory(2L, 2L, 1L, "Leisure", "🥳", ""),
DBCategory(3L, 3L, 1L, "Rent", "🏠", ""),
DBCategory(4L, 4L, 1L, "Bills", "💸", "")
)
categories = cats
categoriesMap = cats.associateBy { it.id }
@@ -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)
}
@@ -103,6 +103,10 @@ fun BillsListScreen(
val context = LocalContext.current
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
var showBetaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
StatefulAlertDialog(
state = viewModel.dialogState,
@@ -158,7 +162,8 @@ fun BillsListScreen(
isArchived = proj?.isArchived == true,
projectType = proj?.type ?: ProjectType.LOCAL,
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
isShareable = proj?.isShareable() ?: true
isShareable = proj?.isShareable() ?: true,
showBetaFeatures = showBetaFeatures
)
}
}
@@ -327,7 +332,7 @@ fun BillsListScreen(
},
actions = {
if (!isSearchExpanded) {
if (viewModel.hasUnlabeledBills) {
if (showBetaFeatures && viewModel.hasUnlabeledBills) {
IconButton(onClick = onLabelBillsClick) {
Icon(
Icons.Default.Category,
@@ -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
@@ -555,11 +554,13 @@ class BillsListViewActivity :
val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId)
val balances = HashMap<Long, Double>()
SupportUtil.getStats(
members, bills,
mutableMapOf(), balances, mutableMapOf(), mutableMapOf(),
-1000, -1000, null, null
-1000L, -1000L, reimbursementCategoryId, null, null
)
Triple(proj, members, balances)
}
@@ -698,15 +699,6 @@ class BillsListViewActivity :
}
}
private fun showDialog(msg: String, title: String, icon: ImageVector) {
viewModel.showDialog(
title = title,
message = msg,
positiveText = getString(android.R.string.ok),
icon = icon
)
}
private fun updateUsernameInDrawer() {
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) {
val text = getString(R.string.drawer_no_account)
@@ -151,14 +151,14 @@ object ProjectImportHelper {
val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null))
val pmRemoteToLocal = mutableMapOf<Long, Long>()
paymentModes.forEach { pm ->
val localId = db.addPaymentMode(DBPaymentMode(0, pm.remoteId, pid, pm.name, pm.icon, pm.color))
pmRemoteToLocal[pm.remoteId] = localId
paymentModes.forEach {
val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color))
pmRemoteToLocal[it.remoteId] = newId
}
val catRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach { cat ->
val localId = db.addCategory(DBCategory(0, cat.remoteId, pid, cat.name, cat.icon, cat.color))
catRemoteToLocal[cat.remoteId] = localId
categories.forEach {
val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color))
catRemoteToLocal[it.remoteId] = newId
}
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
@@ -168,8 +168,8 @@ object ProjectImportHelper {
bills.forEach { b ->
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
val localCatId = catRemoteToLocal[b.categoryId] ?: b.categoryId
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: b.paymentModeId
val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) }
@@ -35,7 +35,8 @@ fun ProjectOptionsDialogContent(
isArchived: Boolean = false,
projectType: ProjectType = ProjectType.LOCAL,
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
isShareable: Boolean = true
isShareable: Boolean = true,
showBetaFeatures: Boolean = false
) {
Surface(
shape = MaterialTheme.shapes.large,
@@ -79,7 +80,7 @@ fun ProjectOptionsDialogContent(
// Row 2: Manage Member, Manage Labels, Manage Currencies
if (!isArchived && isMaintainer) {
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
if (false && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
if (showBetaFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
}
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
@@ -181,7 +182,8 @@ fun ProjectOptionsDialogPreview() {
isArchived = false,
projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true
isShareable = true,
showBetaFeatures = true
)
}
}
@@ -205,7 +207,8 @@ fun ProjectOptionsDialogPreview2() {
isArchived = true,
projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true
isShareable = true,
showBetaFeatures = true
)
}
}
@@ -228,7 +231,8 @@ fun ProjectOptionsDialogPreview3() {
isArchived = false,
projectType = ProjectType.LOCAL,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true
isShareable = true,
showBetaFeatures = true
)
}
}
@@ -320,7 +320,6 @@ class NewProjectActivity : AppCompatActivity() {
pid
)
}
showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG)
return pid
}
@@ -79,6 +79,7 @@ fun SettingsScreen(
val keyColor = stringResource(R.string.pref_key_color)
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
// States for preferences
var nightMode by remember(keyNightMode) {
@@ -113,6 +114,9 @@ fun SettingsScreen(
var showArchived by remember(keyShowArchived) {
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
}
var betaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
Scaffold(
topBar = {
@@ -242,6 +246,19 @@ fun SettingsScreen(
// Other
SettingsCategory(stringResource(R.string.settings_other))
SettingsSwitchPreference(
title = stringResource(R.string.settings_beta_features),
summary = stringResource(R.string.settings_beta_features_summary),
icon = Icons.Default.Info,
checked = betaFeatures,
onCheckedChange = {
betaFeatures = it
sharedPreferences.edit {
putBoolean(keyBetaFeatures, it)
}
}
)
SettingsPreference(
title = stringResource(R.string.title_about),
icon = Icons.Default.Info,
@@ -71,6 +71,7 @@ import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.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,8 +99,11 @@ fun ProjectSankeyDiagram(
var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong() }
val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) {
CategoryUtils.getReimbursementCategoryId(customCategories, projectId, projectRemoteId)
}
val activeBills = remember(allBills, reimbursementCategoryId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != reimbursementCategoryId }
}
val membersMap = remember(allMembers) { allMembers.associateBy { it.id } }
@@ -399,4 +405,4 @@ private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: F
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", 1L, "1", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
@@ -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 == 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"
@@ -1,6 +1,5 @@
package net.helcel.cowspent.model
import android.util.Log
import java.io.Serializable
import java.util.Calendar
import java.util.Locale
@@ -7,16 +7,22 @@ import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.text.TextUtils
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.preference.PreferenceManager
import net.helcel.cowspent.R
import net.helcel.cowspent.android.main.BillsListViewActivity
import net.helcel.cowspent.model.*
import net.helcel.cowspent.model.DBAccountProject
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBBillOwer
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
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.*
import java.util.Locale
/**
* Helps to add, get, update and delete bills, members, projects with the option to trigger a sync with the server.
@@ -156,10 +162,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
@SuppressLint("Range")
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 +205,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 +270,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
)
}
fun deleteProject(id: Long) {
val db = writableDatabase
for (b in getBillsOfProject(id)) {
deleteBill(b.id)
}
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
}
fun updateProject(
projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?,
@@ -647,8 +336,19 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
}
fun deleteProject(id: Long) {
val db = writableDatabase
for (b in getBillsOfProject(id)) {
deleteBill(b.id)
}
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
}
// --- Members logic ---
fun addMember(m: DBMember): Long {
if (BillsListViewActivity.DEBUG) { Log.d(TAG, "[add member]") }
val db = writableDatabase
val values = ContentValues()
values.put(key_remoteId, m.remoteId)
@@ -705,7 +405,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 +479,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 +566,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 +586,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 +700,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,9 +744,445 @@ 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
private const val database_version = 1
private const val database_name = "COWSPENT"
private const val table_members = "MEMBERS"
@@ -1100,16 +1218,16 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
private const val key_repeat = "REPEAT"
private const val key_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"
@@ -264,10 +264,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val members = dbHelper.getMembersOfProject(project.id, null)
val memberIdToRemoteId = members.associate { it.id to it.remoteId }
val categoriesToAdd = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (catToAdd in categoriesToAdd) {
try {
val categoriesResponse = client!!.getCategories(project)
val remoteCategories = categoriesResponse.getCategories(project.id)
val matchingRemote = remoteCategories.find { it.name == catToAdd.name }
if (matchingRemote != null) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, matchingRemote.remoteId)
catToAdd.remoteId = matchingRemote.remoteId
} else {
val createResponse = client!!.createRemoteCategory(project, catToAdd)
val newRemoteId = createResponse.remoteCategoryId
if (newRemoteId > 0) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
catToAdd.remoteId = newRemoteId
}
}
} catch (e: Exception) {
Log.e(TAG, "CATEGORY SYNC FAILED for ${catToAdd.name}", e)
}
}
val categoriesToEdit = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (catToEdit in categoriesToEdit) {
try {
client!!.editRemoteCategory(project, catToEdit)
dbHelper.updateCategory(catToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT CATEGORY FAILED for ${catToEdit.name}, might not exist remotely", e)
// If it fails, we keep the state as EDITED so it tries again next time,
// or we could set it to OK if we think it's a permanent mismatch.
// For now just log it.
}
}
val categoriesToDelete = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (catToDel in categoriesToDelete) {
try {
client!!.deleteRemoteCategory(project, catToDel.remoteId)
dbHelper.deleteCategory(catToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete category on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deleteCategory(catToDel.id)
} else {
throw e
}
} catch (e: Exception) {
Log.e(TAG, "DELETE CATEGORY FAILED for ${catToDel.name}", e)
}
}
val paymentModesToAdd = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_ADDED)
if (paymentModesToAdd.isNotEmpty()) {
try {
val pmsResponse = client!!.getPaymentModes(project)
val remotePms = pmsResponse.getPaymentModes(project.id)
val remotePmsNames = remotePms.map { it.name }
for (pmToAdd in paymentModesToAdd) {
val searchIndex = remotePmsNames.indexOf(pmToAdd.name)
if (searchIndex != -1) {
val remotePm = remotePms[searchIndex]
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, remotePm.remoteId)
pmToAdd.remoteId = remotePm.remoteId
} else {
val createRemotePaymentModeResponse = client!!.createRemotePaymentMode(project, pmToAdd)
val newRemoteId = createRemotePaymentModeResponse.remotePaymentModeId
if (newRemoteId > 0) {
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
pmToAdd.remoteId = newRemoteId
}
}
}
} catch (e: NextcloudHttpRequestFailedException) {
Log.e(TAG, "GET PAYMENT MODES FAILED : " + e.message)
}
}
val paymentModesToEdit = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (pmToEdit in paymentModesToEdit) {
try {
client!!.editRemotePaymentMode(project, pmToEdit)
dbHelper.updatePaymentMode(pmToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT PAYMENT MODE FAILED for ${pmToEdit.name}, might not exist remotely", e)
}
}
val paymentModesToDelete = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (pmToDel in paymentModesToDelete) {
try {
client!!.deleteRemotePaymentMode(project, pmToDel.remoteId)
dbHelper.deletePaymentMode(pmToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete payment mode on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deletePaymentMode(pmToDel.id)
} else {
throw e
}
} catch (_: Exception) {
}
}
val categories = dbHelper.getCategories(project.id)
val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }
val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }.toMutableMap()
// Map hardcoded constants to themselves if not in DB
categoryIdToRemoteId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
categories.filter { it.remoteId < 0 }.forEach { categoryIdToRemoteId[it.remoteId] = it.remoteId }
val paymentModes = dbHelper.getPaymentModes(project.id)
val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }
val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }.toMutableMap()
paymentModes.filter { it.remoteId < 0 }.forEach { paymentModeIdToRemoteId[it.remoteId] = it.remoteId }
val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (bToDel in toDelete) {
@@ -298,11 +409,15 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
for (bToEdit in toEdit) {
try {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) {
val returnedRemoteId = editRemoteBillResponse.remoteBillId
if (returnedRemoteId == bToEdit.remoteId || (returnedRemoteId == 0L && !project.getRequestBaseUrl(true).contains("/ocs/v2.php"))) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else if (returnedRemoteId > 0) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else {
Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)")
}
} catch (_: Exception) {
Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely")
@@ -312,7 +427,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (bToAdd in toAdd) {
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val newRemoteId = createRemoteBillResponse.stringContent.toLong()
val newRemoteId = createRemoteBillResponse.remoteBillId
if (newRemoteId > 0) {
dbHelper.updateBill(
bToAdd.id, newRemoteId, null,
@@ -379,7 +494,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong()
val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId
if (newRemoteId > 0) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
}
@@ -475,7 +590,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localPaymentModes = dbHelper.getPaymentModes(project.id)
for (localPaymentMode in localPaymentModes) {
if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
dbHelper.deletePaymentMode(localPaymentMode.id)
Log.d(TAG, "Delete local pm : $localPaymentMode")
}
@@ -485,6 +600,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId }
for (c in remoteCategories) {
if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue
val localCategory = dbHelper.getCategory(c.remoteId, project.id)
if (localCategory == null) {
Log.d(TAG, "Add local category : $c")
@@ -504,7 +620,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCategories = dbHelper.getCategories(project.id)
for (localCategory in localCategories) {
if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
dbHelper.deleteCategory(localCategory.id)
Log.d(TAG, "Delete local category : $localCategory")
}
@@ -532,7 +648,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCurrencies = dbHelper.getCurrencies(project.id)
for (localCurrency in localCurrencies) {
if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
dbHelper.deleteCurrency(localCurrency.id)
Log.d(TAG, "Delete local currency : $localCurrencies")
}
@@ -599,13 +715,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 }
@@ -617,20 +743,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localBills = dbHelper.getBillsOfProject(project.id)
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
val syncedCategories = dbHelper.getCategories(project.id)
val catRemoteToLocal = syncedCategories.associate { it.remoteId to it.id }
val syncedPaymentModes = dbHelper.getPaymentModes(project.id)
val pmRemoteToLocal = syncedPaymentModes.associate { it.remoteId to it.id }
for (remoteBill in remoteBills) {
if (remoteBill.categoryId > 0) remoteBill.categoryId = catRemoteToLocal[remoteBill.categoryId] ?: 0
if (remoteBill.paymentModeId > 0) remoteBill.paymentModeId = pmRemoteToLocal[remoteBill.paymentModeId] ?: 0
if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) {
dbHelper.addBill(remoteBill)
nbPulledNewBills++
newBillsDialogText += "+ ${remoteBill.what}\n"
Log.d(TAG, "Add local bill : $remoteBill")
} else {
val localBill = localBillsByRemoteId[remoteBill.remoteId]!!
if (hasChanged(localBill, remoteBill)) {
@@ -643,7 +760,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
)
nbPulledUpdatedBills++
updatedBillsDialogText += "${remoteBill.what}\n"
Log.d(TAG, "Update local bill : $remoteBill")
} else {
Log.d(TAG, "Nothing to do for bill : $localBill")
}
@@ -46,4 +46,21 @@ object CategoryUtils {
DBPaymentMode(DBBill.PAYMODE_ID_ONLINE_SERVICE, DBBill.PAYMODE_ID_ONLINE_SERVICE, projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc")
)
}
fun getReimbursementCategoryId(categories: List<DBCategory>, projectId: Long, projectRemoteId: String? = null): Long {
return categories.find {
it.projectId == projectId && (
it.remoteId == DBBill.CATEGORY_REIMBURSEMENT ||
(projectRemoteId != null && it.remoteId.toString() == projectRemoteId)
)
}?.id ?: DBBill.CATEGORY_REIMBURSEMENT
}
fun getCategoryById(context: Context, id: Long, projectId: Long = 0): DBCategory? {
return getDefaultCategories(context, projectId).find { it.id == id }
}
fun getPaymentModeById(context: Context, id: Long, projectId: Long = 0): DBPaymentMode? {
return getDefaultPaymentModes(context, projectId).find { it.id == id }
}
}
@@ -93,6 +93,10 @@ class NextcloudClient(
params: Collection<QueryParam>?,
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) {
@@ -103,13 +107,13 @@ class NextcloudClient(
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 +186,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 +202,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 +252,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) {
@@ -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
@@ -599,8 +823,8 @@ open class ServerResponse(
var comment = ""
var repeat = DBBill.NON_REPEATED
var paymentMode = DBBill.PAYMODE_NONE
var paymentModeId = DBBill.PAYMODE_ID_NONE.toLong()
var categoryId = DBBill.CATEGORY_NONE.toLong()
var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE
var categoryRemoteId = DBBill.CATEGORY_NONE
if (!json.isNull("id")) {
remoteId = json.getLong("id")
}
@@ -639,20 +863,26 @@ open class ServerResponse(
}
if (json.has("paymentmode") && !json.isNull("paymentmode")) {
paymentMode = json.getString("paymentmode")
} else if (json.has("paymentMode") && !json.isNull("paymentMode")) {
paymentMode = json.getString("paymentMode")
}
if (json.has("categoryid") && !json.isNull("categoryid")) {
categoryId = json.getLong("categoryid")
Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId")
categoryRemoteId = json.getLong("categoryid")
} else if (json.has("categoryId") && !json.isNull("categoryId")) {
categoryRemoteId = json.getLong("categoryId")
}
if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) {
paymentModeId = json.getLong("paymentmodeid")
paymentModeRemoteId = json.getLong("paymentmodeid")
} else if (json.has("paymentModeId") && !json.isNull("paymentModeId")) {
paymentModeRemoteId = json.getLong("paymentModeId")
}
// old MB, new Cospend is ok as Cospend provides the old pm ID
// new MB, old Cospend => set payment mode ID from old one
if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeId == DBBill.PAYMODE_ID_NONE.toLong()) {
Log.d("PaymentMode", "old: $paymentMode and new: 0")
paymentModeId = (DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE).toLong()
if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) {
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
}
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
val bill = DBBill(
0, remoteId, projId, payerId, amount, timestamp, what,
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId
@@ -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.categoryId == catId.toLong()) &&
(catId != -100 || b.categoryId != DBBill.CATEGORY_REIMBURSEMENT.toLong()) &&
(paymentModeId == -1000 || b.paymentModeId == paymentModeId.toLong())) &&
((catId == -1000L || catId == -100L || b.categoryId == catId) &&
(catId != -100L || b.categoryId != reimbursementCategoryId) &&
(paymentModeId == -1000L || b.paymentModeId == paymentModeId)) &&
(dateMin == null || b.date >= dateMin) &&
(dateMax == null || b.date <= dateMax)
) {
@@ -285,18 +289,6 @@ object SupportUtil {
return reduceBalance(crediters, debiters, results)
}
@JvmStatic
fun getVersionName(context: Context): String {
var versionName = "0.0.0"
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
versionName = pInfo.versionName ?: "0.0.0"
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return versionName
}
@JvmStatic
fun getJsonObject(text: String?): JSONObject? {
if (text == null) return null
@@ -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, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
@@ -313,10 +313,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0
val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0
paramValues.add(remoteCatId.toString())
paramValues.add(remotePmId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -568,10 +566,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
val remoteCatId = if (bill.categoryId <= 0) bill.categoryId else categoryIdToRemoteId[bill.categoryId] ?: 0
val remotePmId = if (bill.paymentModeId <= 0) bill.paymentModeId else paymentModeIdToRemoteId[bill.paymentModeId] ?: 0
paramValues.add(remoteCatId.toString())
paramValues.add(remotePmId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -650,7 +646,6 @@ class VersatileProjectSyncClient(
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
Log.i(TAG, "using new API for createRemoteBill")
ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members"
@@ -714,7 +709,6 @@ class VersatileProjectSyncClient(
paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
Log.i(TAG, "using new API for getBills")
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills"
@@ -749,6 +743,92 @@ class VersatileProjectSyncClient(
}
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getCategories(project: DBProject): ServerResponse.CategoriesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CategoriesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CategoriesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getPaymentModes(project: DBProject): ServerResponse.PaymentModesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.PaymentModesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.PaymentModesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getMembers(project: DBProject): ServerResponse.MembersResponse {
var target: String
@@ -795,6 +875,305 @@ class VersatileProjectSyncClient(
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.CreateRemoteCategoryResponse {
val paramKeys: MutableList<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()
@@ -952,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) {
@@ -969,13 +1352,13 @@ class VersatileProjectSyncClient(
val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setHeader(headers)
.build()
} else {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setParameter(params)
.setHeader(headers)
.build()
@@ -1009,8 +1392,12 @@ class VersatileProjectSyncClient(
lastETag: String?, username: String?, password: String?,
bearerToken: String?, isOCSRequest: Boolean
): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val httpCon = SupportUtil.getHttpURLConnection(target)
val httpCon = SupportUtil.getHttpURLConnection(finalTarget)
httpCon.requestMethod = method
if (bearerToken != null) {
httpCon.setRequestProperty("Authorization", "Bearer $bearerToken")
@@ -1021,7 +1408,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)
}
+6 -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>
@@ -133,6 +135,8 @@
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
<string name="settings_colorpicker_title">Choose Color</string>
@@ -153,6 +157,7 @@
<string name="pref_key_color_mode" translatable="false">colorMode</string>
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
<string name="pref_key_show_archived" translatable="false">showArchived</string>
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
<string name="pref_value_night_mode_no" translatable="false">1</string>
<string name="pref_value_night_mode_yes" translatable="false">2</string>
<string name="pref_value_night_mode_system" translatable="false">-1</string>
@@ -229,7 +234,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
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}