From 6811d31a6d39f3232d9ee205eb0791b49d2708ac Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoitm@matrix.org>
Date: Thu, 12 Dec 2019 20:24:46 +0100
Subject: [PATCH] Soft Logout - request homeserver login flow

---
 .../vector/riotx/core/error/ErrorFormatter.kt |   1 +
 .../features/signout/SoftLogoutAction.kt      |   1 +
 .../features/signout/SoftLogoutFragment.kt    |  65 +++++---
 .../features/signout/SoftLogoutViewModel.kt   |  81 ++++++++-
 .../features/signout/SoftLogoutViewState.kt   |   2 +
 .../main/res/layout/fragment_soft_logout.xml  | 154 +++++++++++-------
 .../src/main/res/layout/item_error_retry.xml  |   1 -
 7 files changed, 226 insertions(+), 79 deletions(-)

diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt
index d4a2160ed1..e01c2b0542 100644
--- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt
@@ -41,6 +41,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
                         stringProvider.getString(R.string.error_network_timeout)
                     throwable.ioException is UnknownHostException   ->
                         // Invalid homeserver?
+                        // TODO Check network state, airplane mode, etc.
                         stringProvider.getString(R.string.login_error_unknown_host)
                     else                                            ->
                         stringProvider.getString(R.string.error_no_network)
diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt
index da1d4a3adb..a7f25dddba 100644
--- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt
@@ -19,6 +19,7 @@ package im.vector.riotx.features.signout
 import im.vector.riotx.core.platform.VectorViewModelAction
 
 sealed class SoftLogoutAction : VectorViewModelAction {
+    object RetryLoginFlow : SoftLogoutAction()
     data class SignInAgain(val password: String) : SoftLogoutAction()
     // TODO Add reset pwd...
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt
index 1380b9cc5f..52ac4bb8e4 100644
--- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt
@@ -30,18 +30,22 @@ import im.vector.riotx.R
 import im.vector.riotx.core.dialogs.withColoredButton
 import im.vector.riotx.core.error.ErrorFormatter
 import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.extensions.setTextOrHide
 import im.vector.riotx.core.extensions.showPassword
 import im.vector.riotx.core.platform.VectorBaseFragment
 import im.vector.riotx.features.MainActivity
 import im.vector.riotx.features.MainActivityArgs
+import im.vector.riotx.features.login.LoginMode
 import io.reactivex.rxkotlin.subscribeBy
 import kotlinx.android.synthetic.main.fragment_soft_logout.*
+import kotlinx.android.synthetic.main.item_error_retry.*
 import javax.inject.Inject
 
 /**
  * In this screen:
  * - the user is asked to enter a password to sign in again to a homeserver.
  * - or to cleanup all the data
+ * TODO: migrate to Epoxy (along with all the login screen?)
  */
 class SoftLogoutFragment @Inject constructor(
         private val errorFormatter: ErrorFormatter
@@ -67,6 +71,11 @@ class SoftLogoutFragment @Inject constructor(
         }
     }
 
+    @OnClick(R.id.itemErrorRetryButton)
+    fun retry() {
+        softLogoutViewModel.handle(SoftLogoutAction.RetryLoginFlow)
+    }
+
     @OnClick(R.id.softLogoutSubmit)
     fun submit() {
         cleanupUi()
@@ -75,29 +84,36 @@ class SoftLogoutFragment @Inject constructor(
         softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password))
     }
 
+    @OnClick(R.id.softLogoutFormSsoSubmit)
+    fun ssoSubmit() {
+        // TODO
+    }
+
     @OnClick(R.id.softLogoutClearDataSubmit)
-    fun clearData() = withState(softLogoutViewModel) { state ->
-        cleanupUi()
+    fun clearData() {
+        withState(softLogoutViewModel) { state ->
+            cleanupUi()
 
-        val messageResId = if (state.hasUnsavedKeys) {
-            R.string.soft_logout_clear_data_dialog_content
-        } else {
-            R.string.soft_logout_clear_data_dialog_e2e_warning_content
+            val messageResId = if (state.hasUnsavedKeys) {
+                R.string.soft_logout_clear_data_dialog_content
+            } else {
+                R.string.soft_logout_clear_data_dialog_e2e_warning_content
+            }
+
+            AlertDialog.Builder(requireActivity())
+                    .setTitle(R.string.soft_logout_clear_data_dialog_title)
+                    .setMessage(messageResId)
+                    .setNegativeButton(R.string.cancel, null)
+                    .setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ ->
+                        MainActivity.restartApp(requireActivity(), MainActivityArgs(
+                                clearCache = true,
+                                clearCredentials = true,
+                                isUserLoggedOut = true
+                        ))
+                    }
+                    .show()
+                    .withColoredButton(DialogInterface.BUTTON_POSITIVE)
         }
-
-        AlertDialog.Builder(requireActivity())
-                .setTitle(R.string.soft_logout_clear_data_dialog_title)
-                .setMessage(messageResId)
-                .setNegativeButton(R.string.cancel, null)
-                .setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ ->
-                    MainActivity.restartApp(requireActivity(), MainActivityArgs(
-                            clearCache = true,
-                            clearCredentials = true,
-                            isUserLoggedOut = true
-                    ))
-                }
-                .show()
-                .withColoredButton(DialogInterface.BUTTON_POSITIVE)
     }
 
     private fun cleanupUi() {
@@ -114,6 +130,14 @@ class SoftLogoutFragment @Inject constructor(
         softLogoutE2eWarningNotice.isVisible = state.hasUnsavedKeys
     }
 
+    private fun setupForm(state: SoftLogoutViewState) {
+        softLogoutFormLoading.isVisible = state.asyncHomeServerLoginFlowRequest is Loading
+        softLogoutFormSsoSubmit.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso
+        softLogoutFormPassword.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Password
+        softLogoutFormError.isVisible = state.asyncHomeServerLoginFlowRequest is Fail
+        itemErrorRetryText.setTextOrHide((state.asyncHomeServerLoginFlowRequest as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) })
+    }
+
     private fun setupSubmitButton() {
         softLogoutPasswordField.textChanges()
                 .map { it.trim().isNotEmpty() }
@@ -156,6 +180,7 @@ class SoftLogoutFragment @Inject constructor(
 
     override fun invalidate() = withState(softLogoutViewModel) { state ->
         setupUi(state)
+        setupForm(state)
         setupAutoFill()
 
         when (state.asyncLoginAction) {
diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt
index 2ef69ceffb..8e73da0416 100644
--- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt
@@ -20,12 +20,16 @@ import com.airbnb.mvrx.*
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.AuthenticationService
+import im.vector.matrix.android.api.auth.data.LoginFlowResult
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
 import im.vector.riotx.core.di.ActiveSessionHolder
 import im.vector.riotx.core.extensions.hasUnsavedKeys
 import im.vector.riotx.core.extensions.toReducedUrl
 import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.features.login.LoginMode
 
 /**
  *
@@ -33,8 +37,9 @@ import im.vector.riotx.core.platform.VectorViewModel
 class SoftLogoutViewModel @AssistedInject constructor(
         @Assisted initialState: SoftLogoutViewState,
         private val session: Session,
-        private val activeSessionHolder: ActiveSessionHolder)
-    : VectorViewModel<SoftLogoutViewState, SoftLogoutAction>(initialState) {
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val authenticationService: AuthenticationService
+) : VectorViewModel<SoftLogoutViewState, SoftLogoutAction>(initialState) {
 
     @AssistedInject.Factory
     interface Factory {
@@ -63,13 +68,83 @@ class SoftLogoutViewModel @AssistedInject constructor(
 
     private var currentTask: Cancelable? = null
 
+    init {
+        // Get the supported login flow
+        getSupportedLoginFlow()
+    }
+
+    private fun getSupportedLoginFlow() {
+        val homeServerConnectionConfig = session.sessionParams.homeServerConnectionConfig
+
+        currentTask?.cancel()
+        currentTask = null
+        authenticationService.cancelPendingLoginOrRegistration()
+
+        setState {
+            copy(
+                    asyncHomeServerLoginFlowRequest = Loading()
+            )
+        }
+
+        currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
+            override fun onFailure(failure: Throwable) {
+                // TODO _viewEvents.post(LoginViewEvents.Error(failure))
+                setState {
+                    copy(
+                            asyncHomeServerLoginFlowRequest = Fail(failure)
+                    )
+                }
+            }
+
+            override fun onSuccess(data: LoginFlowResult) {
+                when (data) {
+                    is LoginFlowResult.Success            -> {
+                        val loginMode = when {
+                            // SSO login is taken first
+                            data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO }      -> LoginMode.Sso
+                            data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
+                            else                                                                    -> LoginMode.Unsupported
+                        }
+
+                        if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
+                                || loginMode == LoginMode.Unsupported) {
+                            notSupported()
+                        } else {
+                            setState {
+                                copy(
+                                        asyncHomeServerLoginFlowRequest = Success(loginMode)
+                                )
+                            }
+                        }
+                    }
+                    is LoginFlowResult.OutdatedHomeserver -> {
+                        notSupported()
+                    }
+                }
+            }
+
+            private fun notSupported() {
+                // Should not happen since it's a re-logout
+                // Notify the UI
+                // _viewEvents.post(LoginViewEvents.OutdatedHomeserver)
+
+                setState {
+                    copy(
+                            asyncHomeServerLoginFlowRequest = Fail(IllegalStateException("Should not happen"))
+                    )
+                }
+            }
+        })
+    }
+
     // TODO Cleanup
     // private val _viewEvents = PublishDataSource<LoginViewEvents>()
     // val viewEvents: DataSource<LoginViewEvents> = _viewEvents
 
     override fun handle(action: SoftLogoutAction) {
         when (action) {
-            is SoftLogoutAction.SignInAgain -> handleSignInAgain(action)
+            is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow()
+            is SoftLogoutAction.SignInAgain    -> handleSignInAgain(action)
         }
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt
index 87b6a420e1..c58eec821d 100644
--- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt
@@ -20,8 +20,10 @@ import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.Uninitialized
+import im.vector.riotx.features.login.LoginMode
 
 data class SoftLogoutViewState(
+        val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized,
         val asyncLoginAction: Async<Unit> = Uninitialized,
         val homeServerUrl: String,
         val userId: String,
diff --git a/vector/src/main/res/layout/fragment_soft_logout.xml b/vector/src/main/res/layout/fragment_soft_logout.xml
index 0b4727831d..aece715cd6 100644
--- a/vector/src/main/res/layout/fragment_soft_logout.xml
+++ b/vector/src/main/res/layout/fragment_soft_logout.xml
@@ -55,73 +55,117 @@
                 tools:visibility="visible" />
 
             <FrameLayout
-                android:id="@+id/softLogoutPasswordContainer"
+                android:id="@+id/softLogoutFormContainer"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="16dp">
 
-                <com.google.android.material.textfield.TextInputLayout
-                    android:id="@+id/softLogoutPasswordFieldTil"
-                    style="@style/VectorTextInputLayout"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:hint="@string/soft_logout_signin_password_hint"
-                    app:errorEnabled="true"
-                    app:errorIconDrawable="@null">
-
-                    <com.google.android.material.textfield.TextInputEditText
-                        android:id="@+id/softLogoutPasswordField"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:ems="10"
-                        android:inputType="textPassword"
-                        android:maxLines="1"
-                        android:paddingEnd="48dp"
-                        android:paddingRight="48dp"
-                        tools:ignore="RtlSymmetry" />
-
-                </com.google.android.material.textfield.TextInputLayout>
-
-                <ImageView
-                    android:id="@+id/softLogoutPasswordReveal"
-                    android:layout_width="@dimen/layout_touch_size"
-                    android:layout_height="@dimen/layout_touch_size"
-                    android:layout_gravity="end"
-                    android:layout_marginTop="8dp"
-                    android:background="?attr/selectableItemBackground"
-                    android:scaleType="center"
-                    android:src="@drawable/ic_eye_black"
-                    android:tint="?attr/colorAccent"
-                    tools:contentDescription="@string/a11y_show_password" />
-
-            </FrameLayout>
-
-            <RelativeLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:orientation="horizontal">
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/softLogoutForgetPasswordButton"
-                    style="@style/Style.Vector.Login.Button.Text"
+                <!-- Displayed while loading -->
+                <ProgressBar
+                    android:id="@+id/softLogoutFormLoading"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="start"
-                    android:text="@string/auth_forgot_password" />
+                    android:layout_gravity="center_horizontal" />
 
+                <!-- Displayed for SSO mode -->
                 <com.google.android.material.button.MaterialButton
-                    android:id="@+id/softLogoutSubmit"
+                    android:id="@+id/softLogoutFormSsoSubmit"
                     style="@style/Style.Vector.Login.Button"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_alignParentEnd="true"
-                    android:layout_gravity="end"
-                    android:text="@string/soft_logout_signin_submit"
-                    tools:enabled="false"
-                    tools:ignore="RelativeOverlap" />
+                    android:layout_gravity="center_horizontal"
+                    android:text="@string/login_signin_sso"
+                    android:visibility="gone"
+                    tools:visibility="visible" />
 
-            </RelativeLayout>
+                <!-- Displayed in case of error -->
+                <FrameLayout
+                    android:id="@+id/softLogoutFormError"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                    <include layout="@layout/item_error_retry" />
+
+                </FrameLayout>
+
+                <!-- Displayed for password mode -->
+                <LinearLayout
+                    android:id="@+id/softLogoutFormPassword"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:visibility="gone"
+                    tools:visibility="visible">
+
+                    <FrameLayout
+                        android:id="@+id/softLogoutPasswordContainer"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content">
+
+                        <com.google.android.material.textfield.TextInputLayout
+                            android:id="@+id/softLogoutPasswordFieldTil"
+                            style="@style/VectorTextInputLayout"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:hint="@string/soft_logout_signin_password_hint"
+                            app:errorEnabled="true"
+                            app:errorIconDrawable="@null">
+
+                            <com.google.android.material.textfield.TextInputEditText
+                                android:id="@+id/softLogoutPasswordField"
+                                android:layout_width="match_parent"
+                                android:layout_height="wrap_content"
+                                android:ems="10"
+                                android:inputType="textPassword"
+                                android:maxLines="1"
+                                android:paddingEnd="48dp"
+                                android:paddingRight="48dp"
+                                tools:ignore="RtlSymmetry" />
+
+                        </com.google.android.material.textfield.TextInputLayout>
+
+                        <ImageView
+                            android:id="@+id/softLogoutPasswordReveal"
+                            android:layout_width="@dimen/layout_touch_size"
+                            android:layout_height="@dimen/layout_touch_size"
+                            android:layout_gravity="end"
+                            android:layout_marginTop="8dp"
+                            android:background="?attr/selectableItemBackground"
+                            android:scaleType="center"
+                            android:src="@drawable/ic_eye_black"
+                            android:tint="?attr/colorAccent"
+                            tools:contentDescription="@string/a11y_show_password" />
+
+                    </FrameLayout>
+
+                    <RelativeLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="8dp"
+                        android:orientation="horizontal">
+
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/softLogoutForgetPasswordButton"
+                            style="@style/Style.Vector.Login.Button.Text"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="start"
+                            android:text="@string/auth_forgot_password" />
+
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/softLogoutSubmit"
+                            style="@style/Style.Vector.Login.Button"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_alignParentEnd="true"
+                            android:layout_gravity="end"
+                            android:text="@string/soft_logout_signin_submit"
+                            tools:enabled="false"
+                            tools:ignore="RelativeOverlap" />
+
+                    </RelativeLayout>
+                </LinearLayout>
+            </FrameLayout>
 
             <TextView
                 android:layout_width="wrap_content"
diff --git a/vector/src/main/res/layout/item_error_retry.xml b/vector/src/main/res/layout/item_error_retry.xml
index 8e1077e994..996e5f805e 100644
--- a/vector/src/main/res/layout/item_error_retry.xml
+++ b/vector/src/main/res/layout/item_error_retry.xml
@@ -2,7 +2,6 @@
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/progressBar"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="?riotx_background"