From e638f5debcc32285e8c7f3296d819fb06632bfc9 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 4 Apr 2024 13:36:27 +0200 Subject: [PATCH] Create account: user feedback when account name is already taken (#701) * Show snackbar when account can't be created without known reason; show error when account name is taken * Don't crash on empty account name --- .../davdroid/ui/setup/AccountDetailsPage.kt | 33 ++++++++++++++++--- .../bitfire/davdroid/ui/setup/LoginModel.kt | 16 +++++++++ .../bitfire/davdroid/ui/setup/LoginScreen.kt | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt index d608461eb..a9d505168 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt @@ -19,6 +19,7 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.RadioButton +import androidx.compose.material.SnackbarHostState import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Email @@ -29,9 +30,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -43,9 +46,11 @@ import at.bitfire.davdroid.servicedetection.DavResourceFinder import at.bitfire.davdroid.ui.composable.Assistant import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog import at.bitfire.vcard4android.GroupMethod +import kotlinx.coroutines.launch @Composable fun AccountDetailsPage( + snackbarHostState: SnackbarHostState, loginInfo: LoginInfo, foundConfig: DavResourceFinder.Configuration, onBack: () -> Unit, @@ -54,6 +59,9 @@ fun AccountDetailsPage( ) { BackHandler(onBack = onBack) + val context = LocalContext.current + val scope = rememberCoroutineScope() + val resultOrNull by model.createAccountResult.observeAsState() var showExceptionInfo by remember { mutableStateOf(false) } LaunchedEffect(resultOrNull) { @@ -73,9 +81,16 @@ fun AccountDetailsPage( model.createAccountResult.value = null } ) - // TODO else - } + else + scope.launch { + snackbarHostState.showSnackbar(context.getString(R.string.login_account_not_created)) + } + } + else -> {} } + + // reset result + model.createAccountResult.value = null } val suggestedAccountNames = foundConfig.calDAV?.emails ?: emptyList() @@ -86,6 +101,7 @@ fun AccountDetailsPage( AccountDetailsPage_Content( suggestedAccountNames = suggestedAccountNames, accountName = accountName, + accountNameAlreadyExists = model.accountExists(accountName).observeAsState(false).value, onUpdateAccountName = { accountName = it }, onCreateAccount = { model.createAccount( @@ -106,6 +122,7 @@ fun AccountDetailsPage( fun AccountDetailsPage_Content( suggestedAccountNames: List, accountName: String, + accountNameAlreadyExists: Boolean, onUpdateAccountName: (String) -> Unit = {}, groupMethod: GroupMethod, groupMethodReadOnly: Boolean, @@ -114,7 +131,8 @@ fun AccountDetailsPage_Content( ) { Assistant( nextLabel = stringResource(R.string.login_create_account), - onNext = onCreateAccount + onNext = onCreateAccount, + nextEnabled = accountName.isNotBlank() && !accountNameAlreadyExists ) { Column(Modifier.padding(8.dp)) { var expanded by remember { mutableStateOf(false) } @@ -122,10 +140,15 @@ fun AccountDetailsPage_Content( expanded = expanded, onExpandedChange = { expanded = it } ) { + val accountNameLabel = if (accountNameAlreadyExists) + stringResource(R.string.login_account_name_already_taken) + else + stringResource(R.string.login_account_name) OutlinedTextField( value = accountName, onValueChange = onUpdateAccountName, - label = { Text(stringResource(R.string.login_account_name)) }, + label = { Text(accountNameLabel) }, + isError = accountNameAlreadyExists, singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email @@ -227,6 +250,7 @@ fun AccountDetailsPage_Content_Preview() { AccountDetailsPage_Content( suggestedAccountNames = listOf("name1", "name2@example.com"), accountName = "account@example.com", + accountNameAlreadyExists = false, groupMethod = GroupMethod.GROUP_VCARDS, groupMethodReadOnly = false ) @@ -238,6 +262,7 @@ fun AccountDetailsPage_Content_Preview_With_Apostrophe() { AccountDetailsPage_Content( suggestedAccountNames = listOf("name1", "name2@example.com"), accountName = "account'example.com", + accountNameAlreadyExists = true, groupMethod = GroupMethod.CATEGORIES, groupMethodReadOnly = true ) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt index cb909aa3d..dca2d4837 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt @@ -5,12 +5,15 @@ package at.bitfire.davdroid.ui.setup import android.accounts.Account +import android.accounts.AccountManager import android.app.Application import android.content.ContentResolver import android.provider.CalendarContract import androidx.core.os.CancellationSignal +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.InvalidAccountException @@ -77,6 +80,19 @@ class LoginModel @Inject constructor( } + fun accountExists(accountName: String): LiveData = liveData { + val accountType = context.getString(R.string.account_type) + val exists = + if (accountName.isEmpty()) + false + else + AccountManager.get(context) + .getAccountsByType(accountType) + .contains(Account(accountName, accountType)) + emit(exists) + } + + interface CreateAccountResult { class Success(val account: Account): CreateAccountResult class Error(val exception: Exception?): CreateAccountResult diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt index 5e77dfc93..f5ffc7dbe 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt @@ -145,6 +145,7 @@ fun LoginScreen( foundConfig?.let { val context = LocalContext.current AccountDetailsPage( + snackbarHostState = snackbarHostState, loginInfo = loginInfo, foundConfig = it, onBack = { phase = LoginActivity.Phase.LOGIN_TYPE },