From f00db49bda0421e9d21a850f3a80a2ac7e6286e9 Mon Sep 17 00:00:00 2001
From: onurays <onurays@matrix.org>
Date: Wed, 11 Mar 2020 00:16:37 +0300
Subject: [PATCH] Change password function implemented.

Fixes #528
---
 CHANGES.md                                    |  1 +
 .../android/api/auth/AuthenticationService.kt |  6 ++
 .../api/auth/password/PasswordWizard.kt       | 28 ++++++
 .../matrix/android/api/failure/Extensions.kt  |  5 ++
 .../matrix/android/internal/auth/AuthAPI.kt   |  7 ++
 .../auth/DefaultAuthenticationService.kt      | 19 ++++
 .../auth/data/UpdatePasswordParams.kt         | 42 +++++++++
 .../auth/password/DefaultPasswordWizard.kt    | 87 +++++++++++++++++++
 .../crypto/model/rest/UserPasswordAuth.kt     |  2 +-
 .../im/vector/riotx/core/di/FragmentModule.kt |  6 ++
 .../settings/VectorSettingsGeneralFragment.kt | 32 ++++++-
 11 files changed, 230 insertions(+), 5 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt

diff --git a/CHANGES.md b/CHANGES.md
index e7f920d788..19c7b7e618 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
 ===================================================
 
 Features ✨:
+ - Change password (#528)
  - Cross-Signing | Support SSSS secret sharing (#944)
  - Cross-Signing | Verify new session from existing session (#1134)
  - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt
index 140d1c259f..04e53e79f1 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
 import im.vector.matrix.android.api.auth.data.LoginFlowResult
 import im.vector.matrix.android.api.auth.data.SessionParams
 import im.vector.matrix.android.api.auth.login.LoginWizard
+import im.vector.matrix.android.api.auth.password.PasswordWizard
 import im.vector.matrix.android.api.auth.registration.RegistrationWizard
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.util.Cancelable
@@ -89,4 +90,9 @@ interface AuthenticationService {
     fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
                              credentials: Credentials,
                              callback: MatrixCallback<Session>): Cancelable
+
+    /**
+     * Return a PasswordWizard, to update password.
+     */
+    fun getPasswordWizard(): PasswordWizard
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt
new file mode 100644
index 0000000000..cd41d79b0a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2020 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.matrix.android.api.auth.password
+
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.util.Cancelable
+
+interface PasswordWizard {
+
+    /**
+     * Ask the homeserver to update the password with the provided new password.
+     */
+    fun updatePassword(sessionId: String, userId: String, oldPassword: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt
index 5dfb0eab9b..260b3bb8a4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt
@@ -31,3 +31,8 @@ fun Throwable.shouldBeRetried(): Boolean {
     return this is Failure.NetworkConnection
             || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
 }
+
+fun Throwable.isInvalidPassword(): Boolean {
+    return this is Failure.ServerError
+            && error.message == "Invalid password"
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt
index 2f03c99421..f4258acf54 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
 import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
 import im.vector.matrix.android.internal.auth.data.RiotConfig
 import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
+import im.vector.matrix.android.internal.auth.data.UpdatePasswordParams
 import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
 import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
 import im.vector.matrix.android.internal.auth.registration.RegistrationParams
@@ -102,4 +103,10 @@ internal interface AuthAPI {
      */
     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
     fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call<Unit>
+
+    /**
+     * Ask the homeserver to update the password with the provided new password.
+     */
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
+    fun updatePassword(@Body params: UpdatePasswordParams): Call<Unit>
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
index 85c2cdbf3d..1e869dd1d8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
@@ -28,6 +28,7 @@ import im.vector.matrix.android.api.auth.data.Versions
 import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
 import im.vector.matrix.android.api.auth.data.isSupportedBySdk
 import im.vector.matrix.android.api.auth.login.LoginWizard
+import im.vector.matrix.android.api.auth.password.PasswordWizard
 import im.vector.matrix.android.api.auth.registration.RegistrationWizard
 import im.vector.matrix.android.api.failure.Failure
 import im.vector.matrix.android.api.session.Session
@@ -37,6 +38,7 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
 import im.vector.matrix.android.internal.auth.data.RiotConfig
 import im.vector.matrix.android.internal.auth.db.PendingSessionData
 import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
+import im.vector.matrix.android.internal.auth.password.DefaultPasswordWizard
 import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
 import im.vector.matrix.android.internal.di.Unauthenticated
 import im.vector.matrix.android.internal.network.RetrofitFactory
@@ -66,6 +68,7 @@ internal class DefaultAuthenticationService @Inject constructor(
 
     private var currentLoginWizard: LoginWizard? = null
     private var currentRegistrationWizard: RegistrationWizard? = null
+    private var currentPasswordWizard: PasswordWizard? = null
 
     override fun hasAuthenticatedSessions(): Boolean {
         return sessionParamsStore.getLast() != null
@@ -221,6 +224,22 @@ internal class DefaultAuthenticationService @Inject constructor(
                 }
     }
 
+    override fun getPasswordWizard(): PasswordWizard {
+        return currentPasswordWizard
+                ?: let {
+                    sessionParamsStore.getLast()?.homeServerConnectionConfig?.let {
+                        DefaultPasswordWizard(
+                                okHttpClient,
+                                retrofitFactory,
+                                coroutineDispatchers,
+                                it
+                        ).also {
+                            currentPasswordWizard = it
+                        }
+                    } ?: error("HomeServerConnectionConfig is null")
+                }
+    }
+
     override fun cancelPendingLoginOrRegistration() {
         currentLoginWizard = null
         currentRegistrationWizard = null
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt
new file mode 100644
index 0000000000..250bd9ec2e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020 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.matrix.android.internal.auth.data
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
+
+/**
+ * Class to pass request parameters to update the password.
+ */
+@JsonClass(generateAdapter = true)
+internal data class UpdatePasswordParams(
+        @Json(name = "auth")
+        val auth: UserPasswordAuth? = null,
+
+        @Json(name = "new_password")
+        val newPassword: String? = null
+) {
+    companion object {
+        fun create(sessionId: String, userId: String, oldPassword: String, newPassword: String): UpdatePasswordParams {
+            return UpdatePasswordParams(
+                    auth = UserPasswordAuth(session = sessionId, user = userId, password = oldPassword),
+                    newPassword = newPassword
+            )
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt
new file mode 100644
index 0000000000..11c628e69a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2020 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.matrix.android.internal.auth.password
+
+import dagger.Lazy
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import im.vector.matrix.android.api.auth.password.PasswordWizard
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.auth.data.UpdatePasswordParams
+import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
+import im.vector.matrix.android.internal.di.MoshiProvider
+import im.vector.matrix.android.internal.network.RetrofitFactory
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.launchToCallback
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import kotlinx.coroutines.GlobalScope
+import okhttp3.OkHttpClient
+import timber.log.Timber
+
+internal class DefaultPasswordWizard(
+        okHttpClient: Lazy<OkHttpClient>,
+        retrofitFactory: RetrofitFactory,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
+        private val homeServerConnectionConfig: HomeServerConnectionConfig
+) : PasswordWizard {
+
+    private val authAPI = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
+            .create(AuthAPI::class.java)
+
+    override fun updatePassword(sessionId: String, userId: String, oldPassword: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
+        return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+            updatePasswordInternal(sessionId, userId, oldPassword, newPassword)
+        }
+    }
+
+    private suspend fun updatePasswordInternal(sessionId: String, userId: String, oldPassword: String, newPassword: String) {
+        val params = UpdatePasswordParams.create(sessionId, userId, oldPassword, newPassword)
+        try {
+            executeRequest<Unit>(null) {
+                apiCall = authAPI.updatePassword(params)
+            }
+        } catch (throwable: Throwable) {
+            if (throwable is Failure.OtherServerError
+                    && throwable.httpCode == 401
+                    /* Avoid infinite loop */
+                    && params.auth?.session == null) {
+                try {
+                    MoshiProvider.providesMoshi()
+                            .adapter(RegistrationFlowResponse::class.java)
+                            .fromJson(throwable.errorBody)
+                } catch (e: Exception) {
+                    null
+                }?.let {
+                    // Retry with authentication
+                    try {
+                        executeRequest<Unit>(null) {
+                            apiCall = authAPI.updatePassword(
+                                    params.copy(auth = params.auth?.copy(session = it.session))
+                            )
+                        }
+                        return
+                    } catch (failure: Throwable) {
+                        throw failure
+                    }
+                }
+            }
+            throw throwable
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt
index 45ad43a0ef..5e672d4f59 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt
@@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
 import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
 
 /**
- * This class provides the authentication data to delete a device
+ * This class provides the authentication data by using user and password
  */
 @JsonClass(generateAdapter = true)
 data class UserPasswordAuth(
diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index c68972cdd4..cd2f59bc1f 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -74,6 +74,7 @@ import im.vector.riotx.features.roomprofile.RoomProfileFragment
 import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
 import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
 import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
+import im.vector.riotx.features.settings.VectorSettingsGeneralFragment
 import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
 import im.vector.riotx.features.settings.VectorSettingsLabsFragment
 import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
@@ -280,6 +281,11 @@ interface FragmentModule {
     @FragmentKey(VectorSettingsDevicesFragment::class)
     fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
 
+    @Binds
+    @IntoMap
+    @FragmentKey(VectorSettingsGeneralFragment::class)
+    fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment
+
     @Binds
     @IntoMap
     @FragmentKey(PublicRoomsFragment::class)
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
index 0a670e2c5a..f7a2d4735a 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
@@ -35,6 +35,9 @@ import com.bumptech.glide.Glide
 import com.bumptech.glide.load.engine.cache.DiskCache
 import com.google.android.material.textfield.TextInputEditText
 import com.google.android.material.textfield.TextInputLayout
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.AuthenticationService
+import im.vector.matrix.android.api.failure.isInvalidPassword
 import im.vector.riotx.R
 import im.vector.riotx.core.extensions.hideKeyboard
 import im.vector.riotx.core.extensions.showPassword
@@ -56,8 +59,11 @@ import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import java.io.File
+import javax.inject.Inject
 
-class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
+class VectorSettingsGeneralFragment @Inject constructor(
+        private val authenticationService: AuthenticationService
+) : VectorSettingsBaseFragment() {
 
     override var titleRes = R.string.settings_general_title
     override val preferenceXmlRes = R.xml.vector_settings_general
@@ -109,8 +115,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
 
         // Password
         mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
-            notImplemented()
-            // onPasswordUpdateClick()
+            onPasswordUpdateClick()
             false
         }
 
@@ -771,7 +776,26 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
                     val oldPwd = oldPasswordText.text.toString().trim()
                     val newPwd = newPasswordText.text.toString().trim()
 
-                    notImplemented()
+                    authenticationService.getLastAuthenticatedSession()?.let {
+                        showPasswordLoadingView(true)
+                        authenticationService.getPasswordWizard().updatePassword(it.sessionId, it.myUserId, oldPwd, newPwd, object : MatrixCallback<Unit> {
+                            override fun onSuccess(data: Unit) {
+                                showPasswordLoadingView(false)
+                                dialog.dismiss()
+                                activity.toast(R.string.settings_password_updated)
+                            }
+
+                            override fun onFailure(failure: Throwable) {
+                                showPasswordLoadingView(false)
+                                if (failure.isInvalidPassword()) {
+                                    activity.toast(R.string.settings_fail_to_update_password_invalid_current_password)
+                                } else {
+                                    activity.toast(R.string.settings_fail_to_update_password)
+                                }
+                            }
+                        })
+                    }
+
                     /* TODO
                     showPasswordLoadingView(true)