diff --git a/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt b/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt index 7ca7819..8c1a41f 100644 --- a/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt +++ b/app/src/main/java/eu/steffo/twom/activities/LoginActivity.kt @@ -5,7 +5,7 @@ import android.content.Intent import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContract -import eu.steffo.twom.composables.login.LoginScaffold +import eu.steffo.twom.composables.login.components.LoginScaffold class LoginActivity : ComponentActivity() { diff --git a/app/src/main/java/eu/steffo/twom/composables/errorhandling/LocalizableError.kt b/app/src/main/java/eu/steffo/twom/composables/errorhandling/LocalizableError.kt index 75aad02..6cba1bc 100644 --- a/app/src/main/java/eu/steffo/twom/composables/errorhandling/LocalizableError.kt +++ b/app/src/main/java/eu/steffo/twom/composables/errorhandling/LocalizableError.kt @@ -1,8 +1,13 @@ package eu.steffo.twom.composables.errorhandling +import android.util.Log import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.CancellationException + +private const val TAG = "LocalizableError" data class LocalizableError( @StringRes val stringResourceId: Int, @@ -25,3 +30,20 @@ fun LocalizableError?.Display(contents: @Composable (rendered: String) -> Unit) val rendered = this.render() ?: return contents(rendered) } + +suspend fun MutableState.capture( + @StringRes error: Int, + coroutine: suspend () -> Unit, +): Unit? { + try { + coroutine() + } catch (e: CancellationException) { + Log.v(TAG, "Cancelled coroutine execution", e) + return null + } catch (e: Throwable) { + Log.e(TAG, "Captured error during coroutine execution", e) + this.value = LocalizableError(error, e) + return null + } + return Unit +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt b/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt deleted file mode 100644 index c38f62b..0000000 --- a/app/src/main/java/eu/steffo/twom/composables/login/LoginForm.kt +++ /dev/null @@ -1,231 +0,0 @@ -package eu.steffo.twom.composables.login - -import android.util.Log -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Button -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import eu.steffo.twom.R -import eu.steffo.twom.composables.errorhandling.Display -import eu.steffo.twom.composables.errorhandling.ErrorText -import eu.steffo.twom.composables.errorhandling.LocalizableError -import eu.steffo.twom.composables.fields.PasswordField -import eu.steffo.twom.composables.theme.basePadding -import eu.steffo.twom.utils.TwoMGlobals -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.data.LoginFlowResult -import org.matrix.android.sdk.api.auth.login.LoginWizard -import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.failure.MatrixIdFailure -import org.matrix.android.sdk.api.session.Session - - -enum class LoginStep(val step: Int) { - NONE(0), - SERVICE(1), - WELLKNOWN(2), - FLOWS(3), - WIZARD(4), - LOGIN(5), - DONE(6), -} - - -@Composable -@Preview(showBackground = true) -fun LoginForm( - modifier: Modifier = Modifier, - onLogin: (session: Session) -> Unit = {}, -) { - val scope = rememberCoroutineScope() - - var username by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } - - var loginStep by rememberSaveable { mutableStateOf(LoginStep.NONE) } - var error by remember { mutableStateOf(null) } - - suspend fun doLogin() { - error = null - - Log.d("Login", "Getting authentication service...") - loginStep = LoginStep.SERVICE - val auth = TwoMGlobals.matrix.authenticationService() - - Log.d("Login", "Resetting authentication service...") - auth.reset() - - Log.d("Login", "Retrieving .well-known data for: $username") - loginStep = LoginStep.WELLKNOWN - lateinit var wellKnown: WellknownResult - try { - wellKnown = auth.getWellKnownData(username, null) - } catch (e: MatrixIdFailure.InvalidMatrixId) { - Log.d( - "Login", - "User seems to have input an invalid Matrix ID: $username", - e - ) - error = LocalizableError(R.string.login_error_username_invalid) - return - } catch (e: Throwable) { - Log.e( - "Login", - "Something went wrong while retrieving .well-known data for: $username", - e - ) - error = LocalizableError(R.string.login_error_wellknown_generic, e) - return - } - if (wellKnown !is WellknownResult.Prompt) { - Log.w( - "Login", - "Data is not .well-known for: $username" - ) - error = LocalizableError(R.string.login_error_wellknown_missing) - return - } - - Log.d("Login", "Retrieving login flows for: ${wellKnown.homeServerUrl}") - loginStep = LoginStep.FLOWS - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - lateinit var flows: LoginFlowResult - try { - @Suppress("UNUSED_VALUE") - flows = auth.getLoginFlow( - HomeServerConnectionConfig - .Builder() - .withHomeServerUri(wellKnown.homeServerUrl) - .build() - ) - } catch (e: Throwable) { - Log.e( - "Login", - "Something went wrong while retrieving login flows for: ${wellKnown.homeServerUrl}", - e - ) - error = LocalizableError(R.string.login_error_flows_generic, e) - return - } - - Log.d("Login", "Creating login wizard...") - loginStep = LoginStep.WIZARD - lateinit var wizard: LoginWizard - try { - wizard = auth.getLoginWizard() // Why is this stateful? Aargh. - } catch (e: Throwable) { - Log.e( - "Login", - "Something went wrong while setting up the login wizard.", - e - ) - error = LocalizableError(R.string.login_error_wizard_generic, e) - return - } - - Log.d("Login", "Logging in as: $username") - loginStep = LoginStep.LOGIN - lateinit var session: Session - try { - session = wizard.login( - login = username, - password = password, - initialDeviceName = "TwoM (Android)", - ) - } catch (e: Throwable) { - Log.e( - "Login", - "Something went wrong while logging in as: $username", - e - ) - error = LocalizableError(R.string.login_error_login_generic, e) - return - } - - Log.d( - "Login", - "Logged in successfully with session id: ${session.sessionId}" - ) - loginStep = LoginStep.DONE - - onLogin(session) - } - - Column(modifier) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = loginStep.step.toFloat() / LoginStep.DONE.step.toFloat(), - color = if (error != null) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.linearColor - ) - Row(Modifier.basePadding()) { - Text(LocalContext.current.getString(R.string.login_text)) - } - Row(Modifier.basePadding()) { - TextField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = username, - onValueChange = { username = it }, - label = { - Text(LocalContext.current.getString(R.string.login_username_label)) - }, - placeholder = { - Text(LocalContext.current.getString(R.string.login_username_placeholder)) - }, - supportingText = { - Text(LocalContext.current.getString(R.string.login_username_supporting)) - }, - ) - } - Row(Modifier.basePadding()) { - PasswordField( - modifier = Modifier.fillMaxWidth(), - value = password, - onValueChange = { password = it }, - label = { - Text(LocalContext.current.getString(R.string.login_password_label)) - }, - placeholder = { - Text(LocalContext.current.getString(R.string.login_password_placeholder)) - }, - supportingText = { - Text(LocalContext.current.getString(R.string.login_password_supporting)) - }, - ) - } - Row(Modifier.basePadding()) { - Button( - modifier = Modifier.fillMaxWidth(), - enabled = (username != "" && (loginStep == LoginStep.NONE || error != null)), - onClick = { - scope.launch { doLogin() } - }, - ) { - Text(LocalContext.current.getString(R.string.login_complete_text)) - } - } - error.Display { - Row(Modifier.basePadding()) { - ErrorText( - text = it - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/composables/login/components/LoginForm.kt b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginForm.kt new file mode 100644 index 0000000..d713ea4 --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginForm.kt @@ -0,0 +1,113 @@ +package eu.steffo.twom.composables.login.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import eu.steffo.twom.R +import eu.steffo.twom.composables.errorhandling.Display +import eu.steffo.twom.composables.errorhandling.ErrorText +import eu.steffo.twom.composables.fields.PasswordField +import eu.steffo.twom.composables.login.effects.LoginStep +import eu.steffo.twom.composables.login.effects.manageLogin +import eu.steffo.twom.composables.theme.basePadding +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + + +@Composable +@Preview(showBackground = true) +fun LoginForm( + modifier: Modifier = Modifier, + onLogin: (session: Session) -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + var username by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + + val manager = manageLogin() + + Column(modifier) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = manager.step.ord.toFloat() / LoginStep.DONE.ord.toFloat(), + color = if (manager.error != null) { + MaterialTheme.colorScheme.error + } else { + ProgressIndicatorDefaults.linearColor + } + ) + Row(Modifier.basePadding()) { + Text(LocalContext.current.getString(R.string.login_text)) + } + Row(Modifier.basePadding()) { + TextField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + value = username, + onValueChange = { username = it }, + label = { + Text(LocalContext.current.getString(R.string.login_username_label)) + }, + placeholder = { + Text(LocalContext.current.getString(R.string.login_username_placeholder)) + }, + supportingText = { + Text(LocalContext.current.getString(R.string.login_username_supporting)) + }, + ) + } + Row(Modifier.basePadding()) { + PasswordField( + modifier = Modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = { + Text(LocalContext.current.getString(R.string.login_password_label)) + }, + placeholder = { + Text(LocalContext.current.getString(R.string.login_password_placeholder)) + }, + supportingText = { + Text(LocalContext.current.getString(R.string.login_password_supporting)) + }, + ) + } + Row(Modifier.basePadding()) { + Button( + modifier = Modifier.fillMaxWidth(), + enabled = (username != "" && (manager.step == LoginStep.NONE || manager.error != null)), + onClick = { + scope.launch DoLogin@{ + val session = manager.login(username, password) ?: return@DoLogin + onLogin(session) + } + }, + ) { + Text(LocalContext.current.getString(R.string.login_complete_text)) + } + } + manager.error.Display { + Row(Modifier.basePadding()) { + ErrorText( + text = it + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginScaffold.kt similarity index 95% rename from app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt rename to app/src/main/java/eu/steffo/twom/composables/login/components/LoginScaffold.kt index e0015f9..e8bd166 100644 --- a/app/src/main/java/eu/steffo/twom/composables/login/LoginScaffold.kt +++ b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginScaffold.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.composables.login +package eu.steffo.twom.composables.login.components import android.app.Activity import android.content.Intent diff --git a/app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginTopBar.kt similarity index 93% rename from app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt rename to app/src/main/java/eu/steffo/twom/composables/login/components/LoginTopBar.kt index 67636ec..ffc0883 100644 --- a/app/src/main/java/eu/steffo/twom/composables/login/LoginTopBar.kt +++ b/app/src/main/java/eu/steffo/twom/composables/login/components/LoginTopBar.kt @@ -1,4 +1,4 @@ -package eu.steffo.twom.composables.login +package eu.steffo.twom.composables.login.components import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text diff --git a/app/src/main/java/eu/steffo/twom/composables/login/effects/manageLogin.kt b/app/src/main/java/eu/steffo/twom/composables/login/effects/manageLogin.kt new file mode 100644 index 0000000..eb29fee --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/composables/login/effects/manageLogin.kt @@ -0,0 +1,101 @@ +package eu.steffo.twom.composables.login.effects + +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import eu.steffo.twom.R +import eu.steffo.twom.composables.errorhandling.LocalizableError +import eu.steffo.twom.composables.errorhandling.capture +import eu.steffo.twom.utils.TwoMGlobals +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.Session + +private const val TAG = "manageLogin" + +enum class LoginStep(val ord: Int) { + NONE(0), + RESET(1), + WELLKNOWN(2), + FLOWS(3), + WIZARD(4), + LOGIN(5), + DONE(6), +} + +data class LoginManager( + val step: LoginStep, + val error: LocalizableError?, + val login: suspend (username: String, password: String) -> Session?, +) + +@Composable +fun manageLogin(): LoginManager { + var step by remember { mutableStateOf(LoginStep.NONE) } + val error = remember { mutableStateOf(null) } + + suspend fun login(username: String, password: String): Session? { + Log.i(TAG, "Starting login process for: $username") + step = LoginStep.NONE + + Log.d(TAG, "Resetting authentication service...") + step = LoginStep.RESET + val auth = TwoMGlobals.matrix.authenticationService() + error.capture(R.string.login_error_wellknown_generic) { + auth.reset() + } ?: return null + + Log.d(TAG, "Retrieving .well-known data for: $username") + step = LoginStep.WELLKNOWN + lateinit var wellKnown: WellknownResult + error.capture(R.string.login_error_wellknown_generic) { + wellKnown = auth.getWellKnownData( + matrixId = username, + homeServerConnectionConfig = null, + ) + } ?: return null + if (wellKnown !is WellknownResult.Prompt) { + error.value = LocalizableError(R.string.login_error_wellknown_missing) + return null + } + + Log.d(TAG, "Retrieving login flows for: $username") + step = LoginStep.FLOWS + error.capture(R.string.login_error_flows_generic) { + auth.getLoginFlow( + HomeServerConnectionConfig + .Builder() + .withHomeServerUri((wellKnown as WellknownResult.Prompt).homeServerUrl) + .build() + ) + } ?: return null + + Log.d(TAG, "Getting login wizard...") + step = LoginStep.WIZARD + val wizard = auth.getLoginWizard() + + Log.d(TAG, "Logging in as: $username") + step = LoginStep.LOGIN + lateinit var session: Session + error.capture(R.string.login_error_login_generic) { + session = wizard.login( + login = username, + password = password, + initialDeviceName = "TwoM (Android)" + ) + } ?: return null + + Log.i(TAG, "Logged in as: $session") + step = LoginStep.DONE + return session + } + + return LoginManager( + step = step, + error = error.value, + login = ::login, + ) +} \ No newline at end of file