diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt index 8929fec283..76ca9291ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.signout import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.Cancelable /** @@ -31,6 +32,12 @@ interface SignOutService { fun signInAgain(password: String, callback: MatrixCallback): Cancelable + /** + * Update the session with credentials received after SSO + */ + fun updateCredentials(credentials: Credentials, + callback: MatrixCallback): Cancelable + /** * Sign out, and release the session, clear all the session data, including crypto data * @param sigOutFromHomeserver true if the sign out request has to be done diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt index fdbb102cee..17c91011e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt @@ -17,14 +17,21 @@ package im.vector.matrix.android.internal.session.signout import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.GlobalScope import javax.inject.Inject internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, private val signInAgainTask: SignInAgainTask, + private val sessionParamsStore: SessionParamsStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor) : SignOutService { override fun signInAgain(password: String, @@ -36,6 +43,13 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask .executeBy(taskExecutor) } + override fun updateCredentials(credentials: Credentials, + callback: MatrixCallback): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + sessionParamsStore.updateCredentials(credentials) + } + } + override fun signOut(sigOutFromHomeserver: Boolean, callback: MatrixCallback): Cancelable { return signOutTask diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 618b3ea85d..90d6754448 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -55,4 +55,7 @@ sealed class LoginAction : VectorViewModelAction { object ResetSignMode : ResetAction() object ResetLogin : ResetAction() object ResetResetPassword : ResetAction() + + // For the soft logout case + data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 3972c2cdc6..baa4160351 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -102,6 +102,18 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) + is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + } + } + + private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { + setState { + copy( + signMode = SignMode.SignIn, + loginMode = LoginMode.Sso, + homeServerUrl = action.homeServerUrl, + deviceId = action.deviceId + ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index b6babfdb0d..aa0c3aab65 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -34,6 +34,9 @@ data class LoginViewState( val resetPasswordEmail: String? = null, @PersistState val homeServerUrl: String? = null, + // For SSO session recovery + @PersistState + val deviceId: String? = null, // Network result @PersistState diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index eac4511b57..1708b7c982 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -30,10 +30,14 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.activityViewModel +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.AssetReader +import im.vector.riotx.features.signout.soft.SoftLogoutAction +import im.vector.riotx.features.signout.soft.SoftLogoutViewModel import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder @@ -48,9 +52,12 @@ class LoginWebFragment @Inject constructor( private val errorFormatter: ErrorFormatter ) : AbstractLoginFragment() { + private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + override fun getLayoutResId() = R.layout.fragment_login_web private var isWebViewLoaded = false + private var isForSessionRecovery = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -60,6 +67,9 @@ class LoginWebFragment @Inject constructor( override fun updateWithState(state: LoginViewState) { setupTitle(state) + + isForSessionRecovery = state.deviceId?.isNotBlank() == true + if (!isWebViewLoaded) { setupWebView(state) isWebViewLoaded = true @@ -110,13 +120,22 @@ class LoginWebFragment @Inject constructor( } private fun launchWebView(state: LoginViewState) { - if (state.signMode == SignMode.SignIn) { - loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/") - } else { - // MODE_REGISTER - loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/") + val url = buildString { + append(state.homeServerUrl?.trim { it == '/' }) + if (state.signMode == SignMode.SignIn) { + append("/_matrix/static/client/login/") + state.deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + append("?device_id=$it") + } + } else { + // MODE_REGISTER + append("/_matrix/static/client/register/") + } } + loginWebWebView.loadUrl(url) + loginWebWebView.webViewClient = object : WebViewClient() { override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { @@ -212,10 +231,7 @@ class LoginWebFragment @Inject constructor( if (state.signMode == SignMode.SignIn) { try { if (action == "onLogin") { - val credentials = javascriptResponse.credentials - if (credentials != null) { - loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) - } + javascriptResponse.credentials?.let { notifyViewModel(it) } } } catch (e: Exception) { Timber.e(e, "## shouldOverrideUrlLoading() : failed") @@ -224,10 +240,7 @@ class LoginWebFragment @Inject constructor( // MODE_REGISTER // check the required parameters if (action == "onRegistered") { - val credentials = javascriptResponse.credentials - if (credentials != null) { - loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) - } + javascriptResponse.credentials?.let { notifyViewModel(it) } } } } @@ -239,6 +252,14 @@ class LoginWebFragment @Inject constructor( } } + private fun notifyViewModel(credentials: Credentials) { + if (isForSessionRecovery) { + softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials)) + } else { + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) + } + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetLogin) } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt index bc1c654880..fae35ccb07 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.signout.soft +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.riotx.core.platform.VectorViewModelAction sealed class SoftLogoutAction : VectorViewModelAction { @@ -24,5 +25,5 @@ sealed class SoftLogoutAction : VectorViewModelAction { object TogglePassword : SoftLogoutAction() data class SignInAgain(val password: String) : SoftLogoutAction() - // TODO Add reset pwd... + data class WebLoginSuccess(val credentials: Credentials) : SoftLogoutAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt index 7281705e88..36b2f199e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.Success import im.vector.riotx.R import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.toReducedUrl import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.login.LoginMode import im.vector.riotx.features.signout.soft.epoxy.* @@ -71,7 +72,7 @@ class SoftLogoutController @Inject constructor( loginTextItem { id("signText1") text(stringProvider.getString(R.string.soft_logout_signin_notice, - state.homeServerUrl, + state.homeServerUrl.toReducedUrl(), state.userDisplayName, state.userId)) } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt index fa7c868ff8..b0de9200b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt @@ -31,6 +31,8 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.login.AbstractLoginFragment +import im.vector.riotx.features.login.LoginAction +import im.vector.riotx.features.login.LoginMode import im.vector.riotx.features.login.LoginNavigation import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject @@ -56,6 +58,14 @@ class SoftLogoutFragment @Inject constructor( softLogoutViewModel.subscribe(this) { softLogoutViewState -> softLogoutController.update(softLogoutViewState) + + if (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso) { + // Prepare the loginViewModel for a SSO recovery + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId + )) + } } } @@ -84,7 +94,7 @@ class SoftLogoutFragment @Inject constructor( } override fun ssoSubmit() { - // TODO loginSharedActionViewModel.post(LoginNavigation.Sso) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun clearData() { diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt index 944e220aa8..3b3e188ee6 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt @@ -54,8 +54,9 @@ class SoftLogoutViewModel @AssistedInject constructor( val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() val userId = activity.session.myUserId return SoftLogoutViewState( - homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString().toReducedUrl(), + homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString(), userId = userId, + deviceId = activity.session.sessionParams.credentials.deviceId ?: "", userDisplayName = activity.session.getUser(userId)?.displayName ?: userId, hasUnsavedKeys = activity.session.hasUnsavedKeys() ) @@ -139,14 +140,11 @@ class SoftLogoutViewModel @AssistedInject constructor( }) } - // TODO Cleanup - // private val _viewEvents = PublishDataSource() - // val viewEvents: DataSource = _viewEvents - override fun handle(action: SoftLogoutAction) { when (action) { is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow() is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) + is SoftLogoutAction.WebLoginSuccess -> handleWebLoginSuccess(action) is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action) is SoftLogoutAction.TogglePassword -> handleTogglePassword() } @@ -171,6 +169,30 @@ class SoftLogoutViewModel @AssistedInject constructor( } } + private fun handleWebLoginSuccess(action: SoftLogoutAction.WebLoginSuccess) { + setState { + copy( + asyncLoginAction = Loading() + ) + } + currentTask = session.updateCredentials(action.credentials, + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + + override fun onSuccess(data: Unit) { + onSessionRestored() + } + } + ) + + } + private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) { setState { copy( @@ -190,21 +212,25 @@ class SoftLogoutViewModel @AssistedInject constructor( } override fun onSuccess(data: Unit) { - activeSessionHolder.setActiveSession(session) - // Start the sync - session.startSync(true) - - // TODO Configure and start ? Check that the push still works... - setState { - copy( - asyncLoginAction = Success(Unit) - ) - } + onSessionRestored() } } ) } + private fun onSessionRestored() { + activeSessionHolder.setActiveSession(session) + // Start the sync + session.startSync(true) + + // TODO Configure and start ? Check that the push still works... + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + override fun onCleared() { super.onCleared() diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt index 5c8a9101de..01776d1982 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt @@ -24,6 +24,7 @@ data class SoftLogoutViewState( val asyncLoginAction: Async = Uninitialized, val homeServerUrl: String, val userId: String, + val deviceId: String, val userDisplayName: String, val hasUnsavedKeys: Boolean, val passwordShown: Boolean = false,