Warn if logout/delete would impact unsynced data
This commit is contained in:
@@ -109,8 +109,7 @@ class AccountActivity : AppCompatActivity() {
|
||||
viewModel.useSso = false
|
||||
preferences.edit { putBoolean(SETTINGS_USE_SSO, false) }
|
||||
}
|
||||
},
|
||||
onLogout = { viewModel.logout() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
@@ -28,8 +29,7 @@ fun AccountScreen(
|
||||
viewModel: AccountViewModel,
|
||||
onBack: () -> Unit,
|
||||
onConnect: () -> Unit,
|
||||
onSsoClick: (Boolean) -> Unit,
|
||||
onLogout: () -> Unit
|
||||
onSsoClick: (Boolean) -> Unit
|
||||
) {
|
||||
AccountScreenContent(
|
||||
isLoggedIn = viewModel.isLoggedIn,
|
||||
@@ -51,7 +51,57 @@ fun AccountScreen(
|
||||
onBack = onBack,
|
||||
onConnect = onConnect,
|
||||
onSsoClick = onSsoClick,
|
||||
onLogout = onLogout
|
||||
onLogout = { viewModel.requestLogout() }
|
||||
)
|
||||
|
||||
viewModel.logoutImpact?.let { impact ->
|
||||
LogoutConfirmationDialog(
|
||||
impact = impact,
|
||||
onDismiss = { viewModel.cancelLogout() },
|
||||
onConfirm = { viewModel.confirmLogout() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogoutConfirmationDialog(
|
||||
impact: AccountViewModel.LogoutImpact,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.logout_confirm_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
if (impact.projects > 0) {
|
||||
pluralStringResource(
|
||||
R.plurals.logout_confirm_projects, impact.projects, impact.projects
|
||||
)
|
||||
} else {
|
||||
stringResource(R.string.logout_confirm_no_projects)
|
||||
}
|
||||
)
|
||||
if (impact.unsyncedBills > 0) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
R.plurals.warning_unsynced_bills,
|
||||
impact.unsyncedBills,
|
||||
impact.unsyncedBills
|
||||
),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_logout)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.simple_cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -13,11 +14,17 @@ import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.cowspent.model.DBProject
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import net.helcel.cowspent.util.CospendClientUtil
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
|
||||
class AccountViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private companion object {
|
||||
const val TAG = "AccountViewModel"
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
|
||||
var useSso by mutableStateOf(preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false))
|
||||
@@ -43,6 +50,9 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
var showWebView by mutableStateOf(false)
|
||||
|
||||
var isLoggedIn by mutableStateOf(false)
|
||||
|
||||
/** Non-null while the logout confirmation is up, carrying what it would cost. */
|
||||
var logoutImpact by mutableStateOf<LogoutImpact?>(null)
|
||||
private set
|
||||
|
||||
var isValidatingLogin by mutableStateOf(false)
|
||||
@@ -90,25 +100,60 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forgetProjectsTheAccountProvided() {
|
||||
try {
|
||||
val db = CowspentSQLiteOpenHelper.getInstance(getApplication())
|
||||
val offered = db.accountProjects
|
||||
val cospendPath = "/index.php/apps/cospend"
|
||||
for (project in db.projects) {
|
||||
val matches = offered.any {
|
||||
it.remoteId == project.remoteId &&
|
||||
project.serverUrl?.replace("/+$".toRegex(), "") ==
|
||||
it.ncUrl.replace("/+$".toRegex(), "") + cospendPath
|
||||
private fun projectsTheAccountProvided(db: CowspentSQLiteOpenHelper): List<DBProject> {
|
||||
val offered = db.accountProjects
|
||||
val cospendPath = "/index.php/apps/cospend"
|
||||
return db.projects.filter { project ->
|
||||
offered.any {
|
||||
it.remoteId == project.remoteId &&
|
||||
project.serverUrl?.replace("/+$".toRegex(), "") ==
|
||||
it.ncUrl.replace("/+$".toRegex(), "") + cospendPath
|
||||
}
|
||||
}
|
||||
}
|
||||
data class LogoutImpact(val projects: Int, val unsyncedBills: Int)
|
||||
|
||||
private fun measureLogoutImpact(): LogoutImpact {
|
||||
val db = CowspentSQLiteOpenHelper.getInstance(getApplication())
|
||||
val projects = projectsTheAccountProvided(db)
|
||||
val unsynced = projects.sumOf { db.countUnsyncedBills(it.id) }
|
||||
return LogoutImpact(projects.size, unsynced)
|
||||
}
|
||||
|
||||
fun requestLogout() {
|
||||
viewModelScope.launch {
|
||||
logoutImpact = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
measureLogoutImpact()
|
||||
} catch (e: Exception) {
|
||||
// Never let a failed count block signing out - just ask without the detail.
|
||||
Log.e(TAG, "Could not measure what logging out would remove", e)
|
||||
LogoutImpact(0, 0)
|
||||
}
|
||||
if (matches) db.deleteProject(project.id)
|
||||
}
|
||||
db.clearAccountProjects()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AccountViewModel", "Could not remove the account's projects on logout", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLogout() {
|
||||
logoutImpact = null
|
||||
}
|
||||
|
||||
fun confirmLogout() {
|
||||
logoutImpact = null
|
||||
logout()
|
||||
}
|
||||
|
||||
private fun forgetProjectsTheAccountProvided() {
|
||||
try {
|
||||
val db = CowspentSQLiteOpenHelper.getInstance(getApplication())
|
||||
projectsTheAccountProvided(db).forEach { db.deleteProject(it.id) }
|
||||
db.clearAccountProjects()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not remove the account's projects on logout", e)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) { forgetProjectsTheAccountProvided() }
|
||||
|
||||
@@ -408,11 +408,26 @@ class BillsListViewActivity :
|
||||
private fun onRemoveProjectClick(projectId: Long) {
|
||||
if (projectId == 0L) return
|
||||
lifecycleScope.launch {
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
|
||||
|
||||
val (proj, unsyncedBills) = withContext(Dispatchers.IO) {
|
||||
val p = db.getProject(projectId) ?: return@withContext null
|
||||
p to db.countUnsyncedBills(projectId)
|
||||
} ?: return@launch
|
||||
|
||||
val message = buildString {
|
||||
if (!proj.isLocal) append(getString(R.string.dialog_confirm_remove_project_msg))
|
||||
if (unsyncedBills > 0) {
|
||||
if (isNotEmpty()) append("\n\n")
|
||||
append(
|
||||
resources.getQuantityString(
|
||||
R.plurals.warning_unsynced_bills, unsyncedBills, unsyncedBills
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.title_confirm),
|
||||
message = if (!proj.isLocal) getString(R.string.dialog_confirm_remove_project_msg) else null,
|
||||
message = message.ifEmpty { null },
|
||||
positiveText = getString(R.string.simple_yes),
|
||||
onConfirm = {
|
||||
lifecycleScope.launch {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.text.TextUtils
|
||||
@@ -553,6 +554,14 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
fun getBillsOfProject(projId: Long): List<DBBill> {
|
||||
return getBillsCustom("$key_projectid = ?", arrayOf(projId.toString()), "$key_timestamp ASC")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun countUnsyncedBills(projId: Long): Int = DatabaseUtils.queryNumEntries(
|
||||
readableDatabase,
|
||||
table_bills,
|
||||
"$key_projectid = ? AND $key_state != ?",
|
||||
arrayOf(projId.toString(), DBBill.STATE_OK.toString())
|
||||
).toInt()
|
||||
|
||||
fun getBillsOfProjectWithState(projId: Long, state: Int): List<DBBill> {
|
||||
return getBillsCustom(
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_label_bills">Label missing categories</string>
|
||||
<string name="action_logout">Logout</string>
|
||||
<string name="logout_confirm_title">Log out?</string>
|
||||
<string name="logout_confirm_no_projects">You can sign back in at any time.</string>
|
||||
<string name="action_connect">Connect</string>
|
||||
<string name="action_discard">Discard</string>
|
||||
<string name="action_members">Members</string>
|
||||
@@ -299,4 +301,14 @@
|
||||
<item quantity="one">%d project the account offers is kept off this device.</item>
|
||||
<item quantity="other">%d projects the account offers are kept off this device.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="logout_confirm_projects">
|
||||
<item quantity="one">%d project from this account will be removed from this device. It comes back when you sign in again.</item>
|
||||
<item quantity="other">%d projects from this account will be removed from this device. They come back when you sign in again.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="warning_unsynced_bills">
|
||||
<item quantity="one">%d bill has not reached the server yet and will be lost.</item>
|
||||
<item quantity="other">%d bills have not reached the server yet and will be lost.</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user