diff --git a/app/build.gradle b/app/build.gradle index 4b80502..a89c283 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,8 @@ android { targetSdk = 37 versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4" versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -136,6 +138,11 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'androidx.test:core:1.7.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0' + + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-test-manifest' } tasks.register('jacocoTestReport', JacocoReport) { diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt index e02bcbc..6fdd291 100644 --- a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt @@ -48,7 +48,7 @@ import java.util.Locale class AccountActivity : AppCompatActivity() { - private val viewModel: AccountViewModel by viewModels() + internal val viewModel: AccountViewModel by viewModels() companion object { private val TAG = AccountActivity::class.java.simpleName diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index 843b225..cb9a8f6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -34,7 +34,7 @@ import java.util.Calendar class EditBillActivity : AppCompatActivity() { - private val viewModel: EditBillViewModel by viewModels() + internal val viewModel: EditBillViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper private lateinit var bill: DBBill private var projectType: ProjectType = ProjectType.LOCAL diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt index 681fd8e..b366094 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt @@ -17,7 +17,7 @@ import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.ProjectType class LabelBillsActivity : AppCompatActivity() { - private val viewModel: LabelBillsViewModel by viewModels() + internal val viewModel: LabelBillsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt index ad30126..c08eef7 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt @@ -7,14 +7,8 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.helcel.cowspent.R import net.helcel.cowspent.android.helper.showToast -import net.helcel.cowspent.model.DBBill -import net.helcel.cowspent.model.DBCurrency import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.theme.ThemeUtils @@ -23,9 +17,8 @@ import net.helcel.cowspent.util.ICallback class ManageCurrenciesActivity : AppCompatActivity() { - private val viewModel: ManageCurrenciesViewModel by viewModels() + internal val viewModel: ManageCurrenciesViewModel by viewModels() private var db: CowspentSQLiteOpenHelper? = null - private var selectedProjectID: Long = -1 private val editMainCurrencyCallBack: ICallback = object : ICallback { override fun onFinish() {} @@ -47,131 +40,45 @@ class ManageCurrenciesActivity : AppCompatActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) - intent.extras?.let { - selectedProjectID = it.getLong(EXTRA_PROJECT_ID) - } + val selectedProjectID = intent.getLongExtra(EXTRA_PROJECT_ID, -1L) if (selectedProjectID == -1L) { Log.e(TAG, "Missing project id") finish() return } - db = CowspentSQLiteOpenHelper.getInstance(this) - - lifecycleScope.launch { - val project = withContext(Dispatchers.IO) { db!!.getProject(selectedProjectID) } - viewModel.mainCurrencyName = project?.currencyName?.let { if (it == "null") "" else it } ?: "" - updateCurrenciesList() + viewModel.projectId = selectedProjectID + viewModel.loadCurrencies() - setContent { - ThemeUtils.CowspentTheme { - ManageCurrenciesScreen( - viewModel = viewModel, - onBack = { finish() }, - onSaveMain = { saveMainCurrency() }, - onAdd = { addOrUpdateCurrency() }, - onDelete = { deleteCurrency(it) }, - onEdit = { startEditing(it) }, - onCancelEdit = { cancelEditing() } - ) - } + db = CowspentSQLiteOpenHelper.getInstance(this) + + setContent { + ThemeUtils.CowspentTheme { + ManageCurrenciesScreen( + viewModel = viewModel, + onBack = { finish() }, + onSaveMain = { saveMainCurrency() }, + onAdd = { viewModel.addCurrency() }, + onDelete = { viewModel.deleteCurrency(it.id) }, + onEdit = { viewModel.startEditing(it) }, + onCancelEdit = { viewModel.cancelEditing() } + ) } } } private fun saveMainCurrency() { - val newMainCurrencyName = viewModel.mainCurrencyName - lifecycleScope.launch { - withContext(Dispatchers.IO) { - db!!.updateProject( - projId = selectedProjectID, - newCurrencyName = newMainCurrencyName - ) - val project = db!!.getProject(selectedProjectID) - if (project != null) { - db!!.syncIfRemote(project) - if (project.type == ProjectType.COSPEND) { - withContext(Dispatchers.Main) { - if (!db!!.cowspentServerSyncHelper - .editRemoteProject( - projId = selectedProjectID, - newName = project.name, - newMainCurrencyName = newMainCurrencyName, - callback = editMainCurrencyCallBack - ) - ) { - showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG) - } - } - } else { - withContext(Dispatchers.Main) { - showToast(this@ManageCurrenciesActivity, getString(R.string.currency_saved_success), Toast.LENGTH_LONG) - } - } + val project = db?.getProject(viewModel.projectId) + if (project != null) { + if (project.type == ProjectType.COSPEND) { + if (!db!!.cowspentServerSyncHelper.isSyncPossible) { + showToast(this, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG) } + } else { + showToast(this, getString(R.string.currency_saved_success), Toast.LENGTH_LONG) } } - } - - private fun addOrUpdateCurrency() { - val uiRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 } - val exchangeRate = if (uiRate != 0.0) 1.0 / uiRate else 0.0 - val currencyName = viewModel.newCurrencyName - val editingId = viewModel.editingCurrencyId - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - if (editingId != null) { - db!!.updateCurrency(editingId, currencyName, exchangeRate) - val currency = db!!.getCurrency(editingId) - if (currency != null) { - db!!.setCurrencyStateSync(editingId, DBBill.STATE_EDITED) - } - } else { - val newCurrency = DBCurrency( - 0, 0, selectedProjectID, - currencyName, exchangeRate, DBBill.STATE_ADDED - ) - db!!.addCurrencyAndSync(newCurrency) - } - } - cancelEditing() - updateCurrenciesList() - } - } - - private fun startEditing(currency: DBCurrency) { - viewModel.editingCurrencyId = currency.id - viewModel.newCurrencyName = currency.name ?: "" - val uiRate = if (currency.exchangeRate != 0.0) 1.0 / currency.exchangeRate else 0.0 - viewModel.newCurrencyRate = uiRate.toString() - } - - private fun cancelEditing() { - viewModel.editingCurrencyId = null - viewModel.newCurrencyName = "" - viewModel.newCurrencyRate = "" - } - - private fun deleteCurrency(currency: DBCurrency) { - lifecycleScope.launch { - withContext(Dispatchers.IO) { - db!!.setCurrencyStateSync(currency.id, DBBill.STATE_DELETED) - } - updateCurrenciesList() - } - } - - private suspend fun updateCurrenciesList() { - val currenciesDB = withContext(Dispatchers.IO) { - val list = db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_ADDED).toMutableList() - list.addAll(db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_EDITED)) - list.addAll(db!!.getCurrenciesOfProjectWithState(selectedProjectID, DBBill.STATE_OK)) - list - } - withContext(Dispatchers.Main) { - viewModel.currencies = currenciesDB - } + viewModel.saveMainCurrency(editMainCurrencyCallBack) } override fun onSupportNavigateUp(): Boolean { diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt index 9124992..dcb73cc 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -1,6 +1,7 @@ package net.helcel.cowspent.android.currencies import android.annotation.SuppressLint +import android.app.Application import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -41,6 +42,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -347,9 +349,10 @@ fun CurrencyRowPreview() { @Preview(showBackground = true) @Composable fun ManageCurrenciesScreenPreview() { + val application = LocalContext.current.applicationContext as Application MaterialTheme { ManageCurrenciesScreen( - viewModel = ManageCurrenciesViewModel().apply { + viewModel = ManageCurrenciesViewModel(application).apply { mainCurrencyName = "EUR" currencies = listOf( DBCurrency(1, 0, 0, "USD", 1.1, 0), diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt index 9d80694..536beb6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesViewModel.kt @@ -1,14 +1,24 @@ package net.helcel.cowspent.android.currencies +import android.app.Application import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.vector.ImageVector -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.helcel.cowspent.android.helper.DialogState +import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBCurrency +import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.ICallback -class ManageCurrenciesViewModel : ViewModel() { +class ManageCurrenciesViewModel(application: Application) : AndroidViewModel(application) { + var projectId: Long = -1 var mainCurrencyName by mutableStateOf("") var newCurrencyName by mutableStateOf("") var newCurrencyRate by mutableStateOf("") @@ -19,6 +29,105 @@ class ManageCurrenciesViewModel : ViewModel() { var dialogState by mutableStateOf(null) + private val db = CowspentSQLiteOpenHelper.getInstance(application) + + fun loadCurrencies() { + if (projectId == -1L) return + viewModelScope.launch { + val project = withContext(Dispatchers.IO) { db.getProject(projectId) } + mainCurrencyName = project?.currencyName?.let { if (it == "null") "" else it } ?: "" + updateCurrenciesList() + } + } + + suspend fun updateCurrenciesList() { + val currenciesDB = withContext(Dispatchers.IO) { + val list = db.getCurrenciesOfProjectWithState(projectId, DBBill.STATE_ADDED).toMutableList() + list.addAll(db.getCurrenciesOfProjectWithState(projectId, DBBill.STATE_EDITED)) + list.addAll(db.getCurrenciesOfProjectWithState(projectId, DBBill.STATE_OK)) + list + } + withContext(Dispatchers.Main) { + currencies = currenciesDB + } + } + + fun saveMainCurrency(callback: ICallback) { + val newMainCurrencyName = mainCurrencyName + viewModelScope.launch { + withContext(Dispatchers.IO) { + db.updateProject( + projId = projectId, + newCurrencyName = newMainCurrencyName + ) + val project = db.getProject(projectId) + if (project != null) { + db.syncIfRemote(project) + if (project.type == ProjectType.COSPEND) { + withContext(Dispatchers.Main) { + if (!db.cowspentServerSyncHelper + .editRemoteProject( + projId = projectId, + newName = project.name, + newMainCurrencyName = newMainCurrencyName, + callback = callback + ) + ) { + // Handled by activity showing toast + } + } + } + } + } + } + } + + fun addCurrency() { + val uiRate = try { newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 } + val exchangeRate = if (uiRate != 0.0) 1.0 / uiRate else 0.0 + val currencyName = newCurrencyName + val editingId = editingCurrencyId + + viewModelScope.launch { + withContext(Dispatchers.IO) { + if (editingId != null) { + db.updateCurrency(editingId, currencyName, exchangeRate) + db.setCurrencyStateSync(editingId, DBBill.STATE_EDITED) + } else { + val newCurrency = DBCurrency( + 0, 0, projectId, + currencyName, exchangeRate, DBBill.STATE_ADDED + ) + db.addCurrencyAndSync(newCurrency) + } + } + cancelEditing() + updateCurrenciesList() + } + } + + fun deleteCurrency(currencyId: Long) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + db.setCurrencyStateSync(currencyId, DBBill.STATE_DELETED) + } + updateCurrenciesList() + } + } + + fun startEditing(currency: DBCurrency) { + editingCurrencyId = currency.id + newCurrencyName = currency.name ?: "" + val uiRate = if (currency.exchangeRate != 0.0) 1.0 / currency.exchangeRate else 0.0 + newCurrencyRate = uiRate.toString() + } + + fun cancelEditing() { + editingCurrencyId = null + newCurrencyName = "" + newCurrencyRate = "" + } + fun showDialog( title: String? = null, message: String? = null, diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt index d6bfad0..1abf8e0 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt @@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import net.helcel.cowspent.theme.ThemeUtils class LabelManagementActivity : AppCompatActivity() { - private val viewModel: LabelManagementViewModel by viewModels() + internal val viewModel: LabelManagementViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt index 6c1dfcd..6ba244f 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -63,7 +63,7 @@ class BillsListViewActivity : AppCompatActivity(), IRefreshBillsListCallback { - private val viewModel: BillsListViewModel by viewModels() + internal val viewModel: BillsListViewModel by viewModels() companion object { var DEBUG = false diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt index d611f14..0e054b6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt @@ -31,7 +31,7 @@ import net.helcel.cowspent.android.project.ProjectImportHelper class NewProjectActivity : AppCompatActivity() { - private val viewModel: NewProjectViewModel by viewModels() + internal val viewModel: NewProjectViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt index 08422be..1839fa2 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt @@ -23,7 +23,7 @@ import net.helcel.cowspent.android.main.MainConstants class EditProjectActivity : AppCompatActivity() { - private val viewModel: EditProjectViewModel by viewModels() + internal val viewModel: EditProjectViewModel by viewModels() private lateinit var db: CowspentSQLiteOpenHelper private lateinit var project: DBProject diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt index 5dba687..6653d65 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt @@ -22,7 +22,7 @@ import net.helcel.cowspent.theme.ThemeUtils class MemberManagementActivity : AppCompatActivity() { - private val viewModel: MemberManagementViewModel by viewModels() + internal val viewModel: MemberManagementViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt index 5677935..eaf090c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt @@ -47,7 +47,7 @@ class ProjectStatisticsActivity : AppCompatActivity() { } companion object { - private const val EXTRA_PROJECT_ID = "extra_project_id" + internal const val EXTRA_PROJECT_ID = "extra_project_id" fun createIntent(context: Context, projectId: Long): Intent { return Intent(context, ProjectStatisticsActivity::class.java).apply { diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt index d83689c..d235104 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -7,6 +7,7 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.text.TextUtils +import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import androidx.preference.PreferenceManager import net.helcel.cowspent.R @@ -1243,11 +1244,20 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : @Volatile private var instance: CowspentSQLiteOpenHelper? = null + @VisibleForTesting + var overrideInstance: CowspentSQLiteOpenHelper? = null + @JvmStatic fun getInstance(context: Context): CowspentSQLiteOpenHelper { + overrideInstance?.let { return it } return instance ?: synchronized(this) { instance ?: CowspentSQLiteOpenHelper(context.applicationContext).also { instance = it } } } + + @VisibleForTesting + fun resetInstance() { + instance = null + } } } diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index 94f3563..5e0f39c 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -8,6 +8,7 @@ import android.content.SharedPreferences import android.net.ConnectivityManager import android.os.IBinder import android.util.Log +import androidx.annotation.VisibleForTesting import androidx.core.content.edit import androidx.core.graphics.toColorInt import androidx.preference.PreferenceManager @@ -1673,6 +1674,12 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen return instance!! } + @VisibleForTesting + fun resetInstance() { + instance = null + projectIdsToSync.clear() + } + fun isNextcloudAccountConfigured(context: Context): Boolean { val preferences = PreferenceManager.getDefaultSharedPreferences(context) return !preferences.getString(AccountActivity.SETTINGS_URL, AccountActivity.DEFAULT_SETTINGS).isNullOrEmpty() ||