diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index e7cb72544b..2dc2d0ef5f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.auth import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session @@ -62,4 +63,9 @@ interface Authenticator { * @return the associated session if any, or null */ fun getSession(sessionParams: SessionParams): Session? + + /** + * Create a session after a SSO successful login + */ + fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 765f410d3f..949aa6611e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -112,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } + override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + val sessionParams = SessionParams(credentials, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + return sessionManager.getOrCreateSession(sessionParams) + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt index ae75b2737d..e1f963ff3d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt @@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow( @Json(name = "stages") val stages: List? = null -) \ No newline at end of file +) { + + companion object { + // Possible values for type + const val TYPE_LOGIN_SSO = "m.login.sso" + const val TYPE_LOGIN_TOKEN = "m.login.token" + const val TYPE_LOGIN_PASSWORD = "m.login.password" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 66281aad37..ccf3a19202 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -48,6 +48,7 @@ import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginFragment +import im.vector.riotx.features.login.LoginSsoFallbackFragment import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator @@ -127,6 +128,8 @@ interface ScreenComponent { fun inject(loginFragment: LoginFragment) + fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment) + fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(quickReactionFragment: QuickReactionFragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt index be04e6c029..6f4757246b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -16,9 +16,12 @@ package im.vector.riotx.features.login +import im.vector.matrix.android.api.auth.data.Credentials + sealed class LoginActions { data class UpdateHomeServer(val homeServerUrl: String) : LoginActions() data class Login(val login: String, val password: String) : LoginActions() + data class SsoLoginSuccess(val credentials: Credentials) : LoginActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 75fcbb7479..591e4ed474 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,16 +18,30 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.viewModel import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog +import im.vector.riotx.features.home.HomeActivity import javax.inject.Inject class LoginActivity : VectorBaseActivity() { + // Supported navigation actions for this Activity + sealed class Navigation { + object OpenSsoLoginFallback : Navigation() + object GoBack : Navigation() + } + + private val loginViewModel: LoginViewModel by viewModel() + @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -41,6 +55,21 @@ class LoginActivity : VectorBaseActivity() { if (isFirstCreation()) { addFragment(LoginFragment(), R.id.simpleFragmentContainer) } + + loginViewModel.navigationLiveData.observeEvent(this) { + when (it) { + is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer) + is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + + loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { + if (it is Success) { + val intent = HomeActivity.newIntent(this) + startActivity(intent) + finish() + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 5bd7152cbb..7f2f44db1e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.isVisible +import androidx.transition.TransitionManager import com.airbnb.mvrx.* import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges @@ -30,12 +31,11 @@ import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.openUrlInExternalBrowser -import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy -import kotlinx.android.synthetic.main.activity_login.* +import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject @@ -51,7 +51,7 @@ class LoginFragment : VectorBaseFragment() { @Inject lateinit var errorFormatter: ErrorFormatter - override fun getLayoutResId() = R.layout.activity_login + override fun getLayoutResId() = R.layout.fragment_login override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -107,6 +107,12 @@ class LoginFragment : VectorBaseFragment() { .subscribeBy { authenticateButton.isEnabled = it } .disposeOnDestroy() authenticateButton.setOnClickListener { authenticate() } + + authenticateButtonSso.setOnClickListener { openSso() } + } + + private fun openSso() { + viewModel.openSso() } private fun setupPasswordReveal() { @@ -128,25 +134,53 @@ class LoginFragment : VectorBaseFragment() { } override fun invalidate() = withState(viewModel) { state -> + TransitionManager.beginDelayedTransition(login_fragment) + when (state.asyncHomeServerLoginFlowRequest) { - is Loading -> { + is Incomplete -> { progressBar.isVisible = true touchArea.isVisible = true - + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false passwordShown = false renderPasswordField() } - is Fail -> { + is Fail -> { progressBar.isVisible = false touchArea.isVisible = false + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() } - is Success -> { + is Success -> { progressBar.isVisible = false touchArea.isVisible = false - // Check login flow - // TODO + when (state.asyncHomeServerLoginFlowRequest()) { + LoginMode.Password -> { + loginField.isVisible = true + passwordContainer.isVisible = true + authenticateButton.isVisible = true + authenticateButtonSso.isVisible = false + } + LoginMode.Sso -> { + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = true + } + LoginMode.Unsupported -> { + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false + Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() + } + } } } @@ -163,11 +197,8 @@ class LoginFragment : VectorBaseFragment() { touchArea.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() } - is Success -> { - val intent = HomeActivity.newIntent(requireActivity()) - startActivity(intent) - requireActivity().finish() - } + // Success is handled by the LoginActivity + is Success -> Unit } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt new file mode 100644 index 0000000000..d7637cc2cf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +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.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* +import timber.log.Timber +import java.net.URLDecoder + + +/** + * Only login is supported for the moment + */ +class LoginSsoFallbackFragment : VectorBaseFragment() { + + private val viewModel: LoginViewModel by activityViewModel() + + var homeServerUrl: String = "" + + enum class Mode { + MODE_LOGIN, + // Not supported in RiotX for the moment + MODE_REGISTER + } + + // Mode (MODE_LOGIN or MODE_REGISTER) + private var mMode = Mode.MODE_LOGIN + + override fun getLayoutResId() = R.layout.fragment_login_sso_fallback + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupToolbar(login_sso_fallback_toolbar) + requireActivity().setTitle(R.string.login) + + setupWebview() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebview() { + login_sso_fallback_webview.settings.javaScriptEnabled = true + + // Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack + // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) + login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" + + homeServerUrl = viewModel.getHomeServerUrl() + + if (!homeServerUrl.endsWith("/")) { + homeServerUrl += "/" + } + + // AppRTC requires third party cookies to work + val cookieManager = android.webkit.CookieManager.getInstance() + + // clear the cookies must be cleared + if (cookieManager == null) { + launchWebView() + } else { + if (!cookieManager.hasCookies()) { + launchWebView() + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + try { + cookieManager.removeAllCookie() + } catch (e: Exception) { + Timber.e(e, " cookieManager.removeAllCookie() fails") + } + + launchWebView() + } else { + try { + cookieManager.removeAllCookies { launchWebView() } + } catch (e: Exception) { + Timber.e(e, " cookieManager.removeAllCookie() fails") + launchWebView() + } + } + } + } + + private fun launchWebView() { + if (mMode == Mode.MODE_LOGIN) { + login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/") + } else { + // MODE_REGISTER + login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/") + } + + login_sso_fallback_webview.webViewClient = object : WebViewClient() { + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, + error: SslError) { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.ssl_could_not_verify) + .setPositiveButton(R.string.ssl_trust) { dialog, which -> handler.proceed() } + .setNegativeButton(R.string.ssl_do_not_trust) { dialog, which -> handler.cancel() } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + handler.cancel() + dialog.dismiss() + return@OnKeyListener true + } + false + }) + .show() + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + + // on error case, close this activity + viewModel.goBack() + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + login_sso_fallback_toolbar.subtitle = url + } + + override fun onPageFinished(view: WebView, url: String) { + // avoid infinite onPageFinished call + if (url.startsWith("http")) { + // Generic method to make a bridge between JS and the UIWebView + val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" + + " var iframe = document.createElement('iframe');" + + " iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" + + " document.documentElement.appendChild(iframe);" + + " iframe.parentNode.removeChild(iframe); iframe = null;" + + " };" + + view.loadUrl(mxcJavascriptSendObjectMessage) + + if (mMode == Mode.MODE_LOGIN) { + // The function the fallback page calls when the login is complete + val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + + " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + + " };" + + view.loadUrl(mxcJavascriptOnRegistered) + } else { + // MODE_REGISTER + // The function the fallback page calls when the registration is complete + val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" + + " = function(homeserverUrl, userId, accessToken) {" + + " sendObjectMessage({ 'action': 'onRegistered'," + + " 'homeServer': homeserverUrl," + + " 'userId': userId," + + " 'accessToken': accessToken });" + + " };" + + view.loadUrl(mxcJavascriptOnRegistered) + } + } + } + + /** + * Example of (formatted) url for MODE_LOGIN: + * + *
+             * js:{
+             *     "action":"onLogin",
+             *     "credentials":{
+             *         "user_id":"@user:matrix.org",
+             *         "access_token":"[ACCESS_TOKEN]",
+             *         "home_server":"matrix.org",
+             *         "device_id":"[DEVICE_ID]",
+             *         "well_known":{
+             *             "m.homeserver":{
+             *                 "base_url":"https://matrix.org/"
+             *                 }
+             *             }
+             *         }
+             *    }
+             * 
+ * @param view + * @param url + * @return + */ + override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { + if (null != url && url.startsWith("js:")) { + var json = url.substring(3) + var parameters: Map? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + + parameters = adapter.fromJson(json) as Map? + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed") + } + + // succeeds to parse parameters + if (parameters != null) { + val action = parameters["action"] as String + + if (mMode == Mode.MODE_LOGIN) { + try { + if (action == "onLogin") { + val credentials = parameters["credentials"] as Map + + val userId = credentials["user_id"] + val accessToken = credentials["access_token"] + val homeServer = credentials["home_server"] + val deviceId = credentials["device_id"] + + // check if the parameters are defined + if (null != homeServer && null != userId && null != accessToken) { + val credentials = Credentials( + userId = userId, + accessToken = accessToken, + homeServer = homeServer, + deviceId = deviceId, + refreshToken = null + ) + + viewModel.handle(LoginActions.SsoLoginSuccess(credentials)) + } + } + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading() : failed") + } + + } else { + // MODE_REGISTER + // check the required parameters + if (action == "onRegistered") { + // TODO The keys are very strange, this code comes from Riot-Android... + if (parameters.containsKey("homeServer") + && parameters.containsKey("userId") + && parameters.containsKey("accessToken")) { + + // We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756 + // Build on object manually + val credentials = Credentials( + userId = parameters["userId"] as String, + accessToken = parameters["accessToken"] as String, + homeServer = parameters["homeServer"] as String, + // TODO We need deviceId on RiotX... + deviceId = "TODO", + refreshToken = null + ) + + viewModel.handle(LoginActions.SsoLoginSuccess(credentials)) + } + } + } + } + return true + } + + return super.shouldOverrideUrlLoading(view, url) + } + } + } + + override fun onBackPressed(): Boolean { + return if (login_sso_fallback_webview.canGoBack()) { + login_sso_fallback_webview.goBack() + true + } else { + false + } + } +} \ No newline at end of file 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 9eee9d2074..627bf90177 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 @@ -16,6 +16,8 @@ package im.vector.riotx.features.login +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import arrow.core.Try import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -25,10 +27,12 @@ import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, @@ -50,9 +54,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - init { - - } + private val _navigationLiveData = MutableLiveData>() + val navigationLiveData: LiveData> + get() = _navigationLiveData var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null @@ -62,6 +66,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginActions.Login -> handleLogin(action) + is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action) } } @@ -83,14 +88,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { override fun onSuccess(data: Session) { - activeSessionHolder.setActiveSession(data) - data.configureAndStart(pushRuleTriggerListener) - - setState { - copy( - asyncLoginAction = Success(Unit) - ) - } + onSessionCreated(data) } override fun onFailure(failure: Throwable) { @@ -104,6 +102,24 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun onSessionCreated(session: Session) { + activeSessionHolder.setActiveSession(session) + session.configureAndStart(pushRuleTriggerListener) + + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + + private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) { + val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfig!!) + + onSessionCreated(session) + } + + private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) { currentTask?.cancel() @@ -120,18 +136,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // This is invalid setState { copy( - asyncHomeServerLoginFlowRequest = Fail(Throwable("Baf format")) + asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format")) ) } } else { - setState { copy( asyncHomeServerLoginFlowRequest = Loading() ) } - currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback { override fun onFailure(failure: Throwable) { setState { @@ -142,28 +156,41 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onSuccess(data: LoginFlowResponse) { - setState { - copy( - asyncHomeServerLoginFlowRequest = Success(data) - ) + val loginMode = when { + // SSO login is taken first + data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso + data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password + else -> LoginMode.Unsupported } - handleLoginFlowResponse(data) + setState { + copy( + asyncHomeServerLoginFlowRequest = Success(loginMode) + ) + } } }) } } - private fun handleLoginFlowResponse(loginFlowResponse: LoginFlowResponse) { - - } - - override fun onCleared() { super.onCleared() currentTask?.cancel() } + fun openSso() { + // Navigate to SSO + _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.OpenSsoLoginFallback)) + } + + fun goBack() { + // Navigate back + _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.GoBack)) + } + + fun getHomeServerUrl(): String { + return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" + } } \ No newline at end of file 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 32c5c43bed..837c2c7056 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 @@ -19,9 +19,15 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.internal.auth.data.LoginFlowResponse data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState + + +enum class LoginMode { + Password, + Sso, + Unsupported +} \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/fragment_login.xml similarity index 91% rename from vector/src/main/res/layout/activity_login.xml rename to vector/src/main/res/layout/fragment_login.xml index 0b76b461df..ed3eeffb9c 100644 --- a/vector/src/main/res/layout/activity_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -1,6 +1,7 @@ @@ -54,6 +55,7 @@ @@ -114,6 +116,16 @@ android:layout_marginTop="22dp" android:text="@string/auth_login" /> + + diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_sso_fallback.xml new file mode 100644 index 0000000000..30ac1bc9a3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_sso_fallback.xml @@ -0,0 +1,22 @@ + + + + + + + + +