Loading app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLegacyLogin.kt 0 → 100644 +268 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Password import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import at.bitfire.davdroid.R import at.bitfire.davdroid.ui.composable.Assistant import at.bitfire.davdroid.ui.composable.PasswordTextField import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.ui.setup.LoginType import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.utils.AccountHelper object MurenaLegacyLogin : LoginType { override val title = R.string.legacy_murena_login override val helpUrl: Uri get() = "https://doc.e.foundation/support-topics".toUri() override val accountType: String get() = AccountTypes.Murena.accountType @Composable override fun LoginScreen( snackbarHostState: SnackbarHostState, initialLoginInfo: LoginInfo, onLogin: (LoginInfo) -> Unit ) { val context = LocalContext.current var showAccountDialog by remember { mutableStateOf(false) } if (AccountHelper.alreadyHasAccount(context)) { showAccountDialog = true } if (showAccountDialog) { MultipleECloudAccountNotAcceptedDialog { showAccountDialog = false } return } val model: MurenaLegacyLoginModel = hiltViewModel( creationCallback = { factory: MurenaLegacyLoginModel.Factory -> factory.create(loginInfo = initialLoginInfo) } ) val uiState = model.uiState MurenaLegacyLoginScreen( url = uiState.url, onSetUrl = model::setUrl, username = uiState.username, onSetUsername = model::setUsername, password = uiState.password, canContinue = uiState.canContinue, onLogin = { if (uiState.canContinue) onLogin(uiState.asLoginInfo()) } ) } } @Composable fun MurenaLegacyLoginScreen( url: String, onSetUrl: (String) -> Unit = {}, username: String, onSetUsername: (String) -> Unit = {}, password: TextFieldState, canContinue: Boolean, onLogin: () -> Unit = {} ) { val focusRequester = remember { FocusRequester() } Assistant( nextLabel = stringResource(R.string.login_login), nextEnabled = canContinue, onNext = onLogin ) { Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.height(40.dp)) // Murena + e logo (replace with actual logos if available) Icon( painter = painterResource(id = R.drawable.ic_murena_logo), contentDescription = stringResource(R.string.eelo_account_name), tint = Color.Unspecified // To display original logo colors ) Spacer(modifier = Modifier.height(24.dp)) Text( text = stringResource(R.string.login_eelo_title), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(24.dp)) OutlinedTextField( value = url, onValueChange = onSetUrl, label = { Text(stringResource(R.string.login_base_url)) }, placeholder = { Text("murena.io") }, singleLine = true, leadingIcon = { Icon(Icons.Default.Folder, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next ), modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) ) OutlinedTextField( value = username, onValueChange = onSetUsername, label = { Text(stringResource(R.string.login_user_id)) }, singleLine = true, leadingIcon = { Icon(Icons.Default.AccountCircle, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next ), modifier = Modifier.fillMaxWidth() ) // Suggestion buttons to add domain suffixes if (!username.contains("@")) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp) ) { OutlinedButton( onClick = { onSetUsername("$username@murena.io") }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), shape = RoundedCornerShape(12.dp), modifier = Modifier .defaultMinSize(minHeight = 32.dp) ) { Text( text = "@murena.io", style = MaterialTheme.typography.bodySmall ) } OutlinedButton( onClick = { onSetUsername("$username@e.email") }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), shape = RoundedCornerShape(12.dp), modifier = Modifier .defaultMinSize(minHeight = 32.dp) ) { Text( text = "@e.email", style = MaterialTheme.typography.bodySmall ) } } } PasswordTextField( password = password, labelText = stringResource(R.string.login_password), leadingIcon = { Icon(Icons.Default.Password, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, imeAction = ImeAction.Done ), onKeyboardAction = { if (canContinue) onLogin() }, modifier = Modifier.fillMaxWidth() ) } } LaunchedEffect(Unit) { focusRequester.requestFocus() } } @Composable @Preview fun MurenaLegacyLoginScreen_Preview() { MurenaLegacyLoginScreen( url = "", username = "user", password = rememberTextFieldState(""), canContinue = false ) } app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLegacyLoginModel.kt 0 → 100644 +95 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.settings.Credentials import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.util.DavUtils.toURIorNull import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString import at.bitfire.davdroid.util.trimToNull import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import java.net.URI @HiltViewModel(assistedFactory = MurenaLegacyLoginModel.Factory::class) class MurenaLegacyLoginModel @AssistedInject constructor( @Assisted val initialLoginInfo: LoginInfo ): ViewModel() { @AssistedFactory interface Factory { fun create(loginInfo: LoginInfo): MurenaLegacyLoginModel } val defaultUrl = "${URI(BuildConfig.MURENA_BASE_URL).host}" data class UiState( val url: String = "", val username: String = "", val password: TextFieldState = TextFieldState() ) { val urlWithPrefix = if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url" val uri = urlWithPrefix.trim().toURIorNull() val canContinue get() = uri != null && username.isNotEmpty() && password.text.toString().isNotEmpty() fun asLoginInfo(): LoginInfo = LoginInfo( baseUri = uri, credentials = Credentials( username = username.trimToNull(), password = password.text.toString().trimToNull()?.toSensitiveString() ) ) } var uiState by mutableStateOf(UiState()) private set init { uiState = UiState( url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: defaultUrl, username = initialLoginInfo.credentials?.username ?: "", password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: ""), ) } fun setUrl(url: String) { uiState = uiState.copy(url = url) } fun setUsername(username: String) { uiState = uiState.copy(username = username) } } app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt +23 −28 Original line number Original line Diff line number Diff line Loading @@ -79,6 +79,7 @@ import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.ui.setup.LoginType import at.bitfire.davdroid.ui.setup.LoginType import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.network.OAuthMurena import foundation.e.accountmanager.network.OAuthMurena import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading @@ -95,33 +96,6 @@ object MurenaLogin : LoginType { override val accountType: String override val accountType: String get() = AccountTypes.Murena.accountType get() = AccountTypes.Murena.accountType @Composable fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) { val activity = LocalActivity.current AlertDialog( onDismissRequest = {}, confirmButton = { TextButton(onClick = { activity?.finish() onDismiss() }) { Text(stringResource(id = android.R.string.ok)) } }, text = { Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message)) }, tonalElevation = 8.dp ) } fun alreadyHasAccount(context: Context): Boolean { val accountManager = AccountManager.get(context) val accounts = accountManager.getAccountsByType(accountType) return accounts.isNotEmpty() } @Composable @Composable override fun LoginScreen( override fun LoginScreen( snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState, Loading @@ -131,7 +105,7 @@ object MurenaLogin : LoginType { val context = LocalContext.current val context = LocalContext.current var showAccountDialog by remember { mutableStateOf(false) } var showAccountDialog by remember { mutableStateOf(false) } if (alreadyHasAccount(context)) { if (AccountHelper.alreadyHasAccount(context)) { showAccountDialog = true showAccountDialog = true } } Loading Loading @@ -204,6 +178,27 @@ object MurenaLogin : LoginType { } } } } @Composable fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) { val activity = LocalActivity.current AlertDialog( onDismissRequest = {}, confirmButton = { TextButton(onClick = { activity?.finish() onDismiss() }) { Text(stringResource(id = android.R.string.ok)) } }, text = { Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message)) }, tonalElevation = 8.dp ) } @Composable @Composable fun MurenaLoginScreen( fun MurenaLoginScreen( email: String, email: String, Loading app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt +6 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,12 @@ object AccountHelper { return allAccounts.toTypedArray() return allAccounts.toTypedArray() } } fun alreadyHasAccount(context: Context): Boolean { val accountManager = AccountManager.get(context) val accounts = accountManager.getAccountsByType(AccountTypes.Murena.accountType) return accounts.isNotEmpty() } fun isOidcAccount(context: Context, account: Account): Boolean { fun isOidcAccount(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val accountManager = AccountManager.get(context) val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) Loading app/src/main/res/values/e_strings.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -33,4 +33,5 @@ <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string> <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="legacy_murena_login">Legacy Murena.io</string> </resources> </resources> Loading
app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLegacyLogin.kt 0 → 100644 +268 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Password import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import at.bitfire.davdroid.R import at.bitfire.davdroid.ui.composable.Assistant import at.bitfire.davdroid.ui.composable.PasswordTextField import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.ui.setup.LoginType import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.utils.AccountHelper object MurenaLegacyLogin : LoginType { override val title = R.string.legacy_murena_login override val helpUrl: Uri get() = "https://doc.e.foundation/support-topics".toUri() override val accountType: String get() = AccountTypes.Murena.accountType @Composable override fun LoginScreen( snackbarHostState: SnackbarHostState, initialLoginInfo: LoginInfo, onLogin: (LoginInfo) -> Unit ) { val context = LocalContext.current var showAccountDialog by remember { mutableStateOf(false) } if (AccountHelper.alreadyHasAccount(context)) { showAccountDialog = true } if (showAccountDialog) { MultipleECloudAccountNotAcceptedDialog { showAccountDialog = false } return } val model: MurenaLegacyLoginModel = hiltViewModel( creationCallback = { factory: MurenaLegacyLoginModel.Factory -> factory.create(loginInfo = initialLoginInfo) } ) val uiState = model.uiState MurenaLegacyLoginScreen( url = uiState.url, onSetUrl = model::setUrl, username = uiState.username, onSetUsername = model::setUsername, password = uiState.password, canContinue = uiState.canContinue, onLogin = { if (uiState.canContinue) onLogin(uiState.asLoginInfo()) } ) } } @Composable fun MurenaLegacyLoginScreen( url: String, onSetUrl: (String) -> Unit = {}, username: String, onSetUsername: (String) -> Unit = {}, password: TextFieldState, canContinue: Boolean, onLogin: () -> Unit = {} ) { val focusRequester = remember { FocusRequester() } Assistant( nextLabel = stringResource(R.string.login_login), nextEnabled = canContinue, onNext = onLogin ) { Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.height(40.dp)) // Murena + e logo (replace with actual logos if available) Icon( painter = painterResource(id = R.drawable.ic_murena_logo), contentDescription = stringResource(R.string.eelo_account_name), tint = Color.Unspecified // To display original logo colors ) Spacer(modifier = Modifier.height(24.dp)) Text( text = stringResource(R.string.login_eelo_title), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(24.dp)) OutlinedTextField( value = url, onValueChange = onSetUrl, label = { Text(stringResource(R.string.login_base_url)) }, placeholder = { Text("murena.io") }, singleLine = true, leadingIcon = { Icon(Icons.Default.Folder, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next ), modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) ) OutlinedTextField( value = username, onValueChange = onSetUsername, label = { Text(stringResource(R.string.login_user_id)) }, singleLine = true, leadingIcon = { Icon(Icons.Default.AccountCircle, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next ), modifier = Modifier.fillMaxWidth() ) // Suggestion buttons to add domain suffixes if (!username.contains("@")) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp) ) { OutlinedButton( onClick = { onSetUsername("$username@murena.io") }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), shape = RoundedCornerShape(12.dp), modifier = Modifier .defaultMinSize(minHeight = 32.dp) ) { Text( text = "@murena.io", style = MaterialTheme.typography.bodySmall ) } OutlinedButton( onClick = { onSetUsername("$username@e.email") }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), shape = RoundedCornerShape(12.dp), modifier = Modifier .defaultMinSize(minHeight = 32.dp) ) { Text( text = "@e.email", style = MaterialTheme.typography.bodySmall ) } } } PasswordTextField( password = password, labelText = stringResource(R.string.login_password), leadingIcon = { Icon(Icons.Default.Password, null) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, imeAction = ImeAction.Done ), onKeyboardAction = { if (canContinue) onLogin() }, modifier = Modifier.fillMaxWidth() ) } } LaunchedEffect(Unit) { focusRequester.requestFocus() } } @Composable @Preview fun MurenaLegacyLoginScreen_Preview() { MurenaLegacyLoginScreen( url = "", username = "user", password = rememberTextFieldState(""), canContinue = false ) }
app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLegacyLoginModel.kt 0 → 100644 +95 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.ui.setup import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.settings.Credentials import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.util.DavUtils.toURIorNull import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString import at.bitfire.davdroid.util.trimToNull import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import java.net.URI @HiltViewModel(assistedFactory = MurenaLegacyLoginModel.Factory::class) class MurenaLegacyLoginModel @AssistedInject constructor( @Assisted val initialLoginInfo: LoginInfo ): ViewModel() { @AssistedFactory interface Factory { fun create(loginInfo: LoginInfo): MurenaLegacyLoginModel } val defaultUrl = "${URI(BuildConfig.MURENA_BASE_URL).host}" data class UiState( val url: String = "", val username: String = "", val password: TextFieldState = TextFieldState() ) { val urlWithPrefix = if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url" val uri = urlWithPrefix.trim().toURIorNull() val canContinue get() = uri != null && username.isNotEmpty() && password.text.toString().isNotEmpty() fun asLoginInfo(): LoginInfo = LoginInfo( baseUri = uri, credentials = Credentials( username = username.trimToNull(), password = password.text.toString().trimToNull()?.toSensitiveString() ) ) } var uiState by mutableStateOf(UiState()) private set init { uiState = UiState( url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: defaultUrl, username = initialLoginInfo.credentials?.username ?: "", password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: ""), ) } fun setUrl(url: String) { uiState = uiState.copy(url = url) } fun setUsername(username: String) { uiState = uiState.copy(username = username) } }
app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt +23 −28 Original line number Original line Diff line number Diff line Loading @@ -79,6 +79,7 @@ import at.bitfire.davdroid.ui.setup.LoginInfo import at.bitfire.davdroid.ui.setup.LoginType import at.bitfire.davdroid.ui.setup.LoginType import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.AccountTypes import foundation.e.accountmanager.network.OAuthMurena import foundation.e.accountmanager.network.OAuthMurena import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading @@ -95,33 +96,6 @@ object MurenaLogin : LoginType { override val accountType: String override val accountType: String get() = AccountTypes.Murena.accountType get() = AccountTypes.Murena.accountType @Composable fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) { val activity = LocalActivity.current AlertDialog( onDismissRequest = {}, confirmButton = { TextButton(onClick = { activity?.finish() onDismiss() }) { Text(stringResource(id = android.R.string.ok)) } }, text = { Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message)) }, tonalElevation = 8.dp ) } fun alreadyHasAccount(context: Context): Boolean { val accountManager = AccountManager.get(context) val accounts = accountManager.getAccountsByType(accountType) return accounts.isNotEmpty() } @Composable @Composable override fun LoginScreen( override fun LoginScreen( snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState, Loading @@ -131,7 +105,7 @@ object MurenaLogin : LoginType { val context = LocalContext.current val context = LocalContext.current var showAccountDialog by remember { mutableStateOf(false) } var showAccountDialog by remember { mutableStateOf(false) } if (alreadyHasAccount(context)) { if (AccountHelper.alreadyHasAccount(context)) { showAccountDialog = true showAccountDialog = true } } Loading Loading @@ -204,6 +178,27 @@ object MurenaLogin : LoginType { } } } } @Composable fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) { val activity = LocalActivity.current AlertDialog( onDismissRequest = {}, confirmButton = { TextButton(onClick = { activity?.finish() onDismiss() }) { Text(stringResource(id = android.R.string.ok)) } }, text = { Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message)) }, tonalElevation = 8.dp ) } @Composable @Composable fun MurenaLoginScreen( fun MurenaLoginScreen( email: String, email: String, Loading
app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt +6 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,12 @@ object AccountHelper { return allAccounts.toTypedArray() return allAccounts.toTypedArray() } } fun alreadyHasAccount(context: Context): Boolean { val accountManager = AccountManager.get(context) val accounts = accountManager.getAccountsByType(AccountTypes.Murena.accountType) return accounts.isNotEmpty() } fun isOidcAccount(context: Context, account: Account): Boolean { fun isOidcAccount(context: Context, account: Account): Boolean { val accountManager = AccountManager.get(context) val accountManager = AccountManager.get(context) val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) Loading
app/src/main/res/values/e_strings.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -33,4 +33,5 @@ <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string> <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="privacy_policy_title_nav">"Privacy Policy"</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string> <string name="legacy_murena_login">Legacy Murena.io</string> </resources> </resources>