? = null
+)
+
+// MatrixClientServerAPIVersion
+private const val r0_0_1 = "r0.0.1"
+private const val r0_1_0 = "r0.1.0"
+private const val r0_2_0 = "r0.2.0"
+private const val r0_3_0 = "r0.3.0"
+private const val r0_4_0 = "r0.4.0"
+private const val r0_5_0 = "r0.5.0"
+private const val r0_6_0 = "r0.6.0"
+
+// MatrixVersionsFeature
+private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
+private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
+private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
+private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
+
+/**
+ * Return true if the SDK supports this homeserver version
+ */
+fun Versions.isSupportedBySdk(): Boolean {
+ return supportLazyLoadMembers()
+}
+
+/**
+ * Return true if the SDK supports this homeserver version for login and registration
+ */
+fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
+ return !doesServerRequireIdentityServerParam()
+ && doesServerAcceptIdentityAccessToken()
+ && doesServerSeparatesAddAndBind()
+}
+
+/**
+ * Return true if the server support the lazy loading of room members
+ *
+ * @return true if the server support the lazy loading of room members
+ */
+private fun Versions.supportLazyLoadMembers(): Boolean {
+ return supportedVersions?.contains(r0_5_0) == true
+ || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
+}
+
+/**
+ * Indicate if the `id_server` parameter is required when registering with an 3pid,
+ * adding a 3pid or resetting password.
+ */
+private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
+ if (supportedVersions?.contains(r0_6_0) == true) return false
+ return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
+}
+
+/**
+ * Indicate if the `id_access_token` parameter can be safely passed to the homeserver.
+ * Some homeservers may trigger errors if they are not prepared for the new parameter.
+ */
+private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean {
+ return supportedVersions?.contains(r0_6_0) == true
+ || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
+}
+
+private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
+ return supportedVersions?.contains(r0_6_0) == true
+ || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt
new file mode 100644
index 0000000000..6285e866cc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.matrix.android.api.auth.data
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+ *
+ * {
+ * "m.homeserver": {
+ * "base_url": "https://matrix.org"
+ * },
+ * "m.identity_server": {
+ * "base_url": "https://vector.im"
+ * }
+ * "m.integrations": {
+ * "managers": [
+ * {
+ * "api_url": "https://integrations.example.org",
+ * "ui_url": "https://integrations.example.org/ui"
+ * },
+ * {
+ * "api_url": "https://bots.example.org"
+ * }
+ * ]
+ * }
+ * }
+ *
+ */
+@JsonClass(generateAdapter = true)
+data class WellKnown(
+ @Json(name = "m.homeserver")
+ var homeServer: WellKnownBaseConfig? = null,
+
+ @Json(name = "m.identity_server")
+ var identityServer: WellKnownBaseConfig? = null,
+
+ @Json(name = "m.integrations")
+ var integrations: Map? = null
+) {
+ /**
+ * Returns the list of integration managers proposed
+ */
+ fun getIntegrationManagers(): List {
+ val managers = ArrayList()
+ integrations?.get("managers")?.let {
+ (it as? ArrayList<*>)?.let { configs ->
+ configs.forEach { config ->
+ (config as? Map<*, *>)?.let { map ->
+ val apiUrl = map["api_url"] as? String
+ val uiUrl = map["ui_url"] as? String ?: apiUrl
+ if (apiUrl != null
+ && apiUrl.startsWith("https://")
+ && uiUrl!!.startsWith("https://")) {
+ managers.add(WellKnownManagerConfig(
+ apiUrl = apiUrl,
+ uiUrl = uiUrl
+ ))
+ }
+ }
+ }
+ }
+ }
+ return managers
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownBaseConfig.kt
new file mode 100644
index 0000000000..c544ebfdf8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownBaseConfig.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.matrix.android.api.auth.data
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+ *
+ * {
+ * "base_url": "https://vector.im"
+ * }
+ *
+ */
+@JsonClass(generateAdapter = true)
+data class WellKnownBaseConfig(
+ @Json(name = "base_url")
+ val baseURL: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt
new file mode 100644
index 0000000000..33ed412a2a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.matrix.android.api.auth.data
+
+data class WellKnownManagerConfig(
+ val apiUrl : String,
+ val uiUrl: String
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt
new file mode 100644
index 0000000000..d7b2f5d960
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.matrix.android.api.auth.login
+
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.util.Cancelable
+
+interface LoginWizard {
+
+ /**
+ * @param login the login field
+ * @param password the password field
+ * @param deviceName the initial device name
+ * @param callback the matrix callback on which you'll receive the result of authentication.
+ * @return return a [Cancelable]
+ */
+ fun login(login: String,
+ password: String,
+ deviceName: String,
+ callback: MatrixCallback): Cancelable
+
+ /**
+ * Reset user password
+ */
+ fun resetPassword(email: String,
+ newPassword: String,
+ callback: MatrixCallback): Cancelable
+
+ /**
+ * Confirm the new password, once the user has checked his email
+ */
+ fun resetPasswordMailConfirmed(callback: MatrixCallback): Cancelable
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegisterThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegisterThreePid.kt
new file mode 100644
index 0000000000..9ad72edc67
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegisterThreePid.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.matrix.android.api.auth.registration
+
+sealed class RegisterThreePid {
+ data class Email(val email: String) : RegisterThreePid()
+ data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid()
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt
new file mode 100644
index 0000000000..fd75e096d9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationResult.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.matrix.android.api.auth.registration
+
+import im.vector.matrix.android.api.session.Session
+
+// Either a session or an object containing data about registration stages
+sealed class RegistrationResult {
+ data class Success(val session: Session) : RegistrationResult()
+ data class FlowResponse(val flowResult: FlowResult) : RegistrationResult()
+}
+
+data class FlowResult(
+ val missingStages: List,
+ val completedStages: List
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt
new file mode 100644
index 0000000000..9c1e38e31e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/RegistrationWizard.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.matrix.android.api.auth.registration
+
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.util.Cancelable
+
+interface RegistrationWizard {
+
+ fun getRegistrationFlow(callback: MatrixCallback): Cancelable
+
+ fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable
+
+ fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable
+
+ fun acceptTerms(callback: MatrixCallback): Cancelable
+
+ fun dummy(callback: MatrixCallback): Cancelable
+
+ fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable
+
+ fun sendAgainThreePid(callback: MatrixCallback): Cancelable
+
+ fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable
+
+ fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback): Cancelable
+
+ val currentThreePid: String?
+
+ // True when login and password has been sent with success to the homeserver
+ val isRegistrationStarted: Boolean
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt
new file mode 100644
index 0000000000..c3f4864232
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.matrix.android.api.auth.registration
+
+sealed class Stage(open val mandatory: Boolean) {
+
+ // m.login.recaptcha
+ data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
+
+ // m.login.oauth2
+ // m.login.email.identity
+ data class Email(override val mandatory: Boolean) : Stage(mandatory)
+
+ // m.login.msisdn
+ data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
+
+ // m.login.token
+
+ // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
+ // and a password, the dummy stage has to be done
+ data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
+
+ // Undocumented yet: m.login.terms
+ data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory)
+
+ // For unknown stages
+ data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory)
+}
+
+typealias TermPolicies = Map<*, *>
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt
index 6c418ed831..9d42e8388c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt
@@ -34,6 +34,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
+ object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
index 70a982089c..f3f097bcc5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
@@ -31,7 +31,9 @@ data class MatrixError(
@Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data
@Json(name = "limit_type") val limitType: String? = null,
- @Json(name = "admin_contact") val adminUri: String? = null) {
+ @Json(name = "admin_contact") val adminUri: String? = null,
+ // For LIMIT_EXCEEDED
+ @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
companion object {
const val FORBIDDEN = "M_FORBIDDEN"
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt
index 7f3543dec2..8473f50796 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Cancelable.kt
@@ -29,3 +29,5 @@ interface Cancelable {
// no-op
}
}
+
+object NoOpCancellable : Cancelable
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 bfc2b76db7..a1c746a299 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
@@ -17,20 +17,47 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.auth.data.Versions
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.login.ResetPasswordMailConfirmed
+import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
-import retrofit2.http.Body
-import retrofit2.http.GET
-import retrofit2.http.Headers
-import retrofit2.http.POST
+import retrofit2.http.*
/**
* The login REST API.
*/
internal interface AuthAPI {
+ /**
+ * Get the version information of the homeserver
+ */
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
+ fun versions(): Call
+
+ /**
+ * Register to the homeserver
+ * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
+ fun register(@Body registrationParams: RegistrationParams): Call
+
+ /**
+ * Add 3Pid during registration
+ * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
+ * https://github.com/matrix-org/matrix-doc/pull/2290
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken")
+ fun add3Pid(@Path("threePid") threePid: String, @Body params: AddThreePidRegistrationParams): Call
+
+ /**
+ * Validate 3pid
+ */
+ @POST
+ fun validate3Pid(@Url url: String, @Body params: ValidationCodeBody): Call
+
/**
* Get the supported login flow
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
@@ -47,4 +74,16 @@ internal interface AuthAPI {
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call
+
+ /**
+ * Ask the homeserver to reset the password associated with the provided email.
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken")
+ fun resetPassword(@Body params: AddThreePidRegistrationParams): Call
+
+ /**
+ * Ask the homeserver to reset the password with the provided new password once the email is validated.
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
+ fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
index 31a85afbfb..22ed0b9a37 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
@@ -20,8 +20,10 @@ import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
+import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
+import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
@@ -50,7 +52,8 @@ internal abstract class AuthModule {
}
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
- .deleteRealmIfMigrationNeeded()
+ .schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
+ .migration(AuthRealmMigration())
.build()
}
}
@@ -59,5 +62,11 @@ internal abstract class AuthModule {
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
@Binds
- abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator
+ abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore
+
+ @Binds
+ abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService
+
+ @Binds
+ abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator
}
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
new file mode 100644
index 0000000000..e7cf999820
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.matrix.android.internal.auth
+
+import dagger.Lazy
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.AuthenticationService
+import im.vector.matrix.android.api.auth.data.*
+import im.vector.matrix.android.api.auth.login.LoginWizard
+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
+import im.vector.matrix.android.internal.SessionManager
+import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
+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.registration.DefaultRegistrationWizard
+import im.vector.matrix.android.internal.di.Unauthenticated
+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 im.vector.matrix.android.internal.util.toCancelable
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import javax.inject.Inject
+
+internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
+ private val okHttpClient: Lazy,
+ private val retrofitFactory: RetrofitFactory,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val sessionParamsStore: SessionParamsStore,
+ private val sessionManager: SessionManager,
+ private val sessionCreator: SessionCreator,
+ private val pendingSessionStore: PendingSessionStore
+) : AuthenticationService {
+
+ private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
+
+ private var currentLoginWizard: LoginWizard? = null
+ private var currentRegistrationWizard: RegistrationWizard? = null
+
+ override fun hasAuthenticatedSessions(): Boolean {
+ return sessionParamsStore.getLast() != null
+ }
+
+ override fun getLastAuthenticatedSession(): Session? {
+ val sessionParams = sessionParamsStore.getLast()
+ return sessionParams?.let {
+ sessionManager.getOrCreateSession(it)
+ }
+ }
+
+ override fun getSession(sessionParams: SessionParams): Session? {
+ return sessionManager.getOrCreateSession(sessionParams)
+ }
+
+ override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable {
+ pendingSessionData = null
+
+ return GlobalScope.launch(coroutineDispatchers.main) {
+ pendingSessionStore.delete()
+
+ val result = runCatching {
+ getLoginFlowInternal(homeServerConnectionConfig)
+ }
+ result.fold(
+ {
+ if (it is LoginFlowResult.Success) {
+ // The homeserver exists and up to date, keep the config
+ pendingSessionData = PendingSessionData(homeServerConnectionConfig)
+ .also { data -> pendingSessionStore.savePendingSessionData(data) }
+ }
+ callback.onSuccess(it)
+ },
+ {
+ callback.onFailure(it)
+ }
+ )
+ }
+ .toCancelable()
+ }
+
+ private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
+ val authAPI = buildAuthAPI(homeServerConnectionConfig)
+
+ // First check the homeserver version
+ val versions = executeRequest {
+ apiCall = authAPI.versions()
+ }
+
+ if (versions.isSupportedBySdk()) {
+ // Get the login flow
+ val loginFlowResponse = executeRequest {
+ apiCall = authAPI.getLoginFlows()
+ }
+ LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk())
+ } else {
+ // Not supported
+ LoginFlowResult.OutdatedHomeserver
+ }
+ }
+
+ override fun getRegistrationWizard(): RegistrationWizard {
+ return currentRegistrationWizard
+ ?: let {
+ pendingSessionData?.homeServerConnectionConfig?.let {
+ DefaultRegistrationWizard(
+ okHttpClient,
+ retrofitFactory,
+ coroutineDispatchers,
+ sessionCreator,
+ pendingSessionStore
+ ).also {
+ currentRegistrationWizard = it
+ }
+ } ?: error("Please call getLoginFlow() with success first")
+ }
+ }
+
+ override val isRegistrationStarted: Boolean
+ get() = currentRegistrationWizard?.isRegistrationStarted == true
+
+ override fun getLoginWizard(): LoginWizard {
+ return currentLoginWizard
+ ?: let {
+ pendingSessionData?.homeServerConnectionConfig?.let {
+ DefaultLoginWizard(
+ okHttpClient,
+ retrofitFactory,
+ coroutineDispatchers,
+ sessionCreator,
+ pendingSessionStore
+ ).also {
+ currentLoginWizard = it
+ }
+ } ?: error("Please call getLoginFlow() with success first")
+ }
+ }
+
+ override fun cancelPendingLoginOrRegistration() {
+ currentLoginWizard = null
+ currentRegistrationWizard = null
+
+ // Keep only the home sever config
+ // Update the local pendingSessionData synchronously
+ pendingSessionData = pendingSessionData?.homeServerConnectionConfig
+ ?.let { PendingSessionData(it) }
+ .also {
+ GlobalScope.launch(coroutineDispatchers.main) {
+ if (it == null) {
+ // Should not happen
+ pendingSessionStore.delete()
+ } else {
+ pendingSessionStore.savePendingSessionData(it)
+ }
+ }
+ }
+ }
+
+ override fun reset() {
+ currentLoginWizard = null
+ currentRegistrationWizard = null
+
+ pendingSessionData = null
+
+ GlobalScope.launch(coroutineDispatchers.main) {
+ pendingSessionStore.delete()
+ }
+ }
+
+ override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
+ credentials: Credentials,
+ callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ createSessionFromSso(credentials, homeServerConnectionConfig)
+ }
+ }
+
+ private suspend fun createSessionFromSso(credentials: Credentials,
+ homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
+ sessionCreator.createSession(credentials, homeServerConnectionConfig)
+ }
+
+ 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/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt
deleted file mode 100644
index ff49d4308b..0000000000
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.matrix.android.internal.auth
-
-import android.util.Patterns
-import dagger.Lazy
-import im.vector.matrix.android.api.MatrixCallback
-import im.vector.matrix.android.api.auth.Authenticator
-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
-import im.vector.matrix.android.api.util.Cancelable
-import im.vector.matrix.android.internal.SessionManager
-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.ThreePidMedium
-import im.vector.matrix.android.internal.di.Unauthenticated
-import im.vector.matrix.android.internal.extensions.foldToCallback
-import im.vector.matrix.android.internal.network.RetrofitFactory
-import im.vector.matrix.android.internal.network.executeRequest
-import im.vector.matrix.android.internal.util.CancelableCoroutine
-import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import okhttp3.OkHttpClient
-import javax.inject.Inject
-
-internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
- private val okHttpClient: Lazy,
- private val retrofitFactory: RetrofitFactory,
- private val coroutineDispatchers: MatrixCoroutineDispatchers,
- private val sessionParamsStore: SessionParamsStore,
- private val sessionManager: SessionManager
-) : Authenticator {
-
- override fun hasAuthenticatedSessions(): Boolean {
- return sessionParamsStore.getLast() != null
- }
-
- override fun getLastAuthenticatedSession(): Session? {
- val sessionParams = sessionParamsStore.getLast()
- return sessionParams?.let {
- sessionManager.getOrCreateSession(it)
- }
- }
-
- override fun getSession(sessionParams: SessionParams): Session? {
- return sessionManager.getOrCreateSession(sessionParams)
- }
-
- override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable {
- val job = GlobalScope.launch(coroutineDispatchers.main) {
- val result = runCatching {
- getLoginFlowInternal(homeServerConnectionConfig)
- }
- result.foldToCallback(callback)
- }
- return CancelableCoroutine(job)
- }
-
- override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
- login: String,
- password: String,
- callback: MatrixCallback): Cancelable {
- val job = GlobalScope.launch(coroutineDispatchers.main) {
- val sessionOrFailure = runCatching {
- authenticate(homeServerConnectionConfig, login, password)
- }
- sessionOrFailure.foldToCallback(callback)
- }
- return CancelableCoroutine(job)
- }
-
- private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
- val authAPI = buildAuthAPI(homeServerConnectionConfig)
-
- executeRequest {
- apiCall = authAPI.getLoginFlows()
- }
- }
-
- private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
- login: String,
- password: String) = withContext(coroutineDispatchers.io) {
- val authAPI = buildAuthAPI(homeServerConnectionConfig)
- val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
- PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, "Mobile")
- } else {
- PasswordLoginParams.userIdentifier(login, password, "Mobile")
- }
- val credentials = executeRequest {
- apiCall = authAPI.login(loginParams)
- }
- val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
- sessionParamsStore.save(sessionParams)
- sessionManager.getOrCreateSession(sessionParams)
- }
-
- override fun createSessionFromSso(credentials: Credentials,
- homeServerConnectionConfig: HomeServerConnectionConfig,
- callback: MatrixCallback): Cancelable {
- val job = GlobalScope.launch(coroutineDispatchers.main) {
- val sessionOrFailure = runCatching {
- createSessionFromSso(credentials, homeServerConnectionConfig)
- }
- sessionOrFailure.foldToCallback(callback)
- }
- return CancelableCoroutine(job)
- }
-
- private suspend fun createSessionFromSso(credentials: Credentials,
- homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
- val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
- sessionParamsStore.save(sessionParams)
- 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/PendingSessionStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/PendingSessionStore.kt
new file mode 100644
index 0000000000..ed28de6ae8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/PendingSessionStore.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.matrix.android.internal.auth
+
+import im.vector.matrix.android.internal.auth.db.PendingSessionData
+
+/**
+ * Store for elements when doing login or registration
+ */
+internal interface PendingSessionStore {
+
+ suspend fun savePendingSessionData(pendingSessionData: PendingSessionData)
+
+ fun getPendingSessionData(): PendingSessionData?
+
+ suspend fun delete()
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt
new file mode 100644
index 0000000000..f04f262d6e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.matrix.android.internal.auth
+
+import android.net.Uri
+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
+import im.vector.matrix.android.internal.SessionManager
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface SessionCreator {
+ suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
+}
+
+internal class DefaultSessionCreator @Inject constructor(
+ private val sessionParamsStore: SessionParamsStore,
+ private val sessionManager: SessionManager,
+ private val pendingSessionStore: PendingSessionStore
+) : SessionCreator {
+
+ /**
+ * Credentials can affect the homeServerConnectionConfig, override home server url and/or
+ * identity server url if provided in the credentials
+ */
+ override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
+ // We can cleanup the pending session params
+ pendingSessionStore.delete()
+
+ val sessionParams = SessionParams(
+ credentials = credentials,
+ homeServerConnectionConfig = homeServerConnectionConfig.copy(
+ homeServerUri = credentials.wellKnown?.homeServer?.baseURL
+ // remove trailing "/"
+ ?.trim { it == '/' }
+ ?.takeIf { it.isNotBlank() }
+ ?.also { Timber.d("Overriding homeserver url to $it") }
+ ?.let { Uri.parse(it) }
+ ?: homeServerConnectionConfig.homeServerUri,
+ identityServerUri = credentials.wellKnown?.identityServer?.baseURL
+ // remove trailing "/"
+ ?.trim { it == '/' }
+ ?.takeIf { it.isNotBlank() }
+ ?.also { Timber.d("Overriding identity server url to $it") }
+ ?.let { Uri.parse(it) }
+ ?: homeServerConnectionConfig.identityServerUri
+ ))
+
+ sessionParamsStore.save(sessionParams)
+ return sessionManager.getOrCreateSession(sessionParams)
+ }
+}
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 a6c027900f..a6d74a8de7 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,12 +30,4 @@ data class InteractiveAuthenticationFlow(
@Json(name = "stages")
val stages: List? = null
-) {
-
- 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"
- }
-}
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt
index 81196c7414..4ff29d594a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt
@@ -25,4 +25,7 @@ object LoginFlowTypes {
const val MSISDN = "m.login.msisdn"
const val RECAPTCHA = "m.login.recaptcha"
const val DUMMY = "m.login.dummy"
+ const val TERMS = "m.login.terms"
+ const val TOKEN = "m.login.token"
+ const val SSO = "m.login.sso"
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt
index 39b1dd8760..f467b4d3a0 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt
@@ -19,34 +19,46 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+/**
+ * Ref:
+ * - https://matrix.org/docs/spec/client_server/r0.5.0#password-based
+ * - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types
+ */
@JsonClass(generateAdapter = true)
-internal data class PasswordLoginParams(@Json(name = "identifier") val identifier: Map,
- @Json(name = "password") val password: String,
- @Json(name = "type") override val type: String,
- @Json(name = "initial_device_display_name") val deviceDisplayName: String?,
- @Json(name = "device_id") val deviceId: String?) : LoginParams {
+internal data class PasswordLoginParams(
+ @Json(name = "identifier") val identifier: Map,
+ @Json(name = "password") val password: String,
+ @Json(name = "type") override val type: String,
+ @Json(name = "initial_device_display_name") val deviceDisplayName: String?,
+ @Json(name = "device_id") val deviceId: String?) : LoginParams {
companion object {
+ private const val IDENTIFIER_KEY_TYPE = "type"
- val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
- val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
- val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
+ private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
+ private const val IDENTIFIER_KEY_USER = "user"
- val IDENTIFIER_KEY_TYPE = "type"
- val IDENTIFIER_KEY_MEDIUM = "medium"
- val IDENTIFIER_KEY_ADDRESS = "address"
- val IDENTIFIER_KEY_USER = "user"
- val IDENTIFIER_KEY_COUNTRY = "country"
- val IDENTIFIER_KEY_NUMBER = "number"
+ private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
+ private const val IDENTIFIER_KEY_MEDIUM = "medium"
+ private const val IDENTIFIER_KEY_ADDRESS = "address"
+
+ private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
+ private const val IDENTIFIER_KEY_COUNTRY = "country"
+ private const val IDENTIFIER_KEY_PHONE = "phone"
fun userIdentifier(user: String,
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
- val identifier = HashMap()
- identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER
- identifier[IDENTIFIER_KEY_USER] = user
- return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
+ return PasswordLoginParams(
+ mapOf(
+ IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
+ IDENTIFIER_KEY_USER to user
+ ),
+ password,
+ LoginFlowTypes.PASSWORD,
+ deviceDisplayName,
+ deviceId)
}
fun thirdPartyIdentifier(medium: String,
@@ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
- val identifier = HashMap()
- identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY
- identifier[IDENTIFIER_KEY_MEDIUM] = medium
- identifier[IDENTIFIER_KEY_ADDRESS] = address
- return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
+ return PasswordLoginParams(
+ mapOf(
+ IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
+ IDENTIFIER_KEY_MEDIUM to medium,
+ IDENTIFIER_KEY_ADDRESS to address
+ ),
+ password,
+ LoginFlowTypes.PASSWORD,
+ deviceDisplayName,
+ deviceId)
+ }
+
+ fun phoneIdentifier(country: String,
+ phone: String,
+ password: String,
+ deviceDisplayName: String? = null,
+ deviceId: String? = null): PasswordLoginParams {
+ return PasswordLoginParams(
+ mapOf(
+ IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
+ IDENTIFIER_KEY_COUNTRY to country,
+ IDENTIFIER_KEY_PHONE to phone
+ ),
+ password,
+ LoginFlowTypes.PASSWORD,
+ deviceDisplayName,
+ deviceId)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
new file mode 100644
index 0000000000..5f1efb487b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 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.db
+
+import io.realm.DynamicRealm
+import io.realm.RealmMigration
+import timber.log.Timber
+
+internal class AuthRealmMigration : RealmMigration {
+
+ companion object {
+ // Current schema version
+ const val SCHEMA_VERSION = 1L
+ }
+
+ override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
+ Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
+
+ if (oldVersion <= 0) {
+ Timber.d("Step 0 -> 1")
+ Timber.d("Create PendingSessionEntity")
+
+ realm.schema.create("PendingSessionEntity")
+ .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
+ .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
+ .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
+ .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
+ .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
+ .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
+ .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
+ .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
+ .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
+ .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmModule.kt
index dcc0393569..ee930cd1ef 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmModule.kt
@@ -23,6 +23,7 @@ import io.realm.annotations.RealmModule
*/
@RealmModule(library = true,
classes = [
- SessionParamsEntity::class
+ SessionParamsEntity::class,
+ PendingSessionEntity::class
])
internal class AuthRealmModule
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt
new file mode 100644
index 0000000000..0314491d3b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionData.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.matrix.android.internal.auth.db
+
+import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import im.vector.matrix.android.internal.auth.login.ResetPasswordData
+import im.vector.matrix.android.internal.auth.registration.ThreePidData
+import java.util.*
+
+/**
+ * This class holds all pending data when creating a session, either by login or by register
+ */
+internal data class PendingSessionData(
+ val homeServerConnectionConfig: HomeServerConnectionConfig,
+
+ /* ==========================================================================================
+ * Common
+ * ========================================================================================== */
+
+ val clientSecret: String = UUID.randomUUID().toString(),
+ val sendAttempt: Int = 0,
+
+ /* ==========================================================================================
+ * For login
+ * ========================================================================================== */
+
+ val resetPasswordData: ResetPasswordData? = null,
+
+ /* ==========================================================================================
+ * For register
+ * ========================================================================================== */
+
+ val currentSession: String? = null,
+ val isRegistrationStarted: Boolean = false,
+ val currentThreePidData: ThreePidData? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionEntity.kt
new file mode 100644
index 0000000000..d21c515849
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionEntity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.matrix.android.internal.auth.db
+
+import io.realm.RealmObject
+
+internal open class PendingSessionEntity(
+ var homeServerConnectionConfigJson: String = "",
+ var clientSecret: String = "",
+ var sendAttempt: Int = 0,
+ var resetPasswordDataJson: String? = null,
+ var currentSession: String? = null,
+ var isRegistrationStarted: Boolean = false,
+ var currentThreePidDataJson: String? = null
+) : RealmObject()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionMapper.kt
new file mode 100644
index 0000000000..32e6ba963e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/PendingSessionMapper.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.matrix.android.internal.auth.db
+
+import com.squareup.moshi.Moshi
+import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import im.vector.matrix.android.internal.auth.login.ResetPasswordData
+import im.vector.matrix.android.internal.auth.registration.ThreePidData
+import javax.inject.Inject
+
+internal class PendingSessionMapper @Inject constructor(moshi: Moshi) {
+
+ private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
+ private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
+ private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
+
+ fun map(entity: PendingSessionEntity?): PendingSessionData? {
+ if (entity == null) {
+ return null
+ }
+
+ val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!!
+ val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) }
+ val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) }
+
+ return PendingSessionData(
+ homeServerConnectionConfig = homeServerConnectionConfig,
+ clientSecret = entity.clientSecret,
+ sendAttempt = entity.sendAttempt,
+ resetPasswordData = resetPasswordData,
+ currentSession = entity.currentSession,
+ isRegistrationStarted = entity.isRegistrationStarted,
+ currentThreePidData = threePidData)
+ }
+
+ fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
+ if (sessionData == null) {
+ return null
+ }
+
+ val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
+ val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
+ val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
+
+ return PendingSessionEntity(
+ homeServerConnectionConfigJson = homeServerConnectionConfigJson,
+ clientSecret = sessionData.clientSecret,
+ sendAttempt = sessionData.sendAttempt,
+ resetPasswordDataJson = resetPasswordDataJson,
+ currentSession = sessionData.currentSession,
+ isRegistrationStarted = sessionData.isRegistrationStarted,
+ currentThreePidDataJson = currentThreePidDataJson
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmPendingSessionStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmPendingSessionStore.kt
new file mode 100644
index 0000000000..6841e43ef0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmPendingSessionStore.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.matrix.android.internal.auth.db
+
+import im.vector.matrix.android.internal.auth.PendingSessionStore
+import im.vector.matrix.android.internal.database.awaitTransaction
+import im.vector.matrix.android.internal.di.AuthDatabase
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import javax.inject.Inject
+
+internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
+ @AuthDatabase
+ private val realmConfiguration: RealmConfiguration
+) : PendingSessionStore {
+
+ override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) {
+ awaitTransaction(realmConfiguration) { realm ->
+ val entity = mapper.map(pendingSessionData)
+ if (entity != null) {
+ realm.where(PendingSessionEntity::class.java)
+ .findAll()
+ .deleteAllFromRealm()
+
+ realm.insert(entity)
+ }
+ }
+ }
+
+ override fun getPendingSessionData(): PendingSessionData? {
+ return Realm.getInstance(realmConfiguration).use { realm ->
+ realm
+ .where(PendingSessionEntity::class.java)
+ .findAll()
+ .map { mapper.map(it) }
+ .firstOrNull()
+ }
+ }
+
+ override suspend fun delete() {
+ awaitTransaction(realmConfiguration) {
+ it.where(PendingSessionEntity::class.java)
+ .findAll()
+ .deleteAllFromRealm()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
index 00fde2682e..dfe35c363b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
@@ -30,36 +30,33 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
) : SessionParamsStore {
override fun getLast(): SessionParams? {
- val realm = Realm.getInstance(realmConfiguration)
- val sessionParams = realm
- .where(SessionParamsEntity::class.java)
- .findAll()
- .map { mapper.map(it) }
- .lastOrNull()
- realm.close()
- return sessionParams
+ return Realm.getInstance(realmConfiguration).use { realm ->
+ realm
+ .where(SessionParamsEntity::class.java)
+ .findAll()
+ .map { mapper.map(it) }
+ .lastOrNull()
+ }
}
override fun get(userId: String): SessionParams? {
- val realm = Realm.getInstance(realmConfiguration)
- val sessionParams = realm
- .where(SessionParamsEntity::class.java)
- .equalTo(SessionParamsEntityFields.USER_ID, userId)
- .findAll()
- .map { mapper.map(it) }
- .firstOrNull()
- realm.close()
- return sessionParams
+ return Realm.getInstance(realmConfiguration).use { realm ->
+ realm
+ .where(SessionParamsEntity::class.java)
+ .equalTo(SessionParamsEntityFields.USER_ID, userId)
+ .findAll()
+ .map { mapper.map(it) }
+ .firstOrNull()
+ }
}
override fun getAll(): List {
- val realm = Realm.getInstance(realmConfiguration)
- val sessionParams = realm
- .where(SessionParamsEntity::class.java)
- .findAll()
- .mapNotNull { mapper.map(it) }
- realm.close()
- return sessionParams
+ return Realm.getInstance(realmConfiguration).use { realm ->
+ realm
+ .where(SessionParamsEntity::class.java)
+ .findAll()
+ .mapNotNull { mapper.map(it) }
+ }
}
override suspend fun save(sessionParams: SessionParams) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt
new file mode 100644
index 0000000000..b847773682
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.matrix.android.internal.auth.login
+
+import android.util.Patterns
+import dagger.Lazy
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.auth.login.LoginWizard
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.api.util.NoOpCancellable
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.auth.PendingSessionStore
+import im.vector.matrix.android.internal.auth.SessionCreator
+import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
+import im.vector.matrix.android.internal.auth.data.ThreePidMedium
+import im.vector.matrix.android.internal.auth.db.PendingSessionData
+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.RegisterAddThreePidTask
+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 kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+
+internal class DefaultLoginWizard(
+ okHttpClient: Lazy,
+ retrofitFactory: RetrofitFactory,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val sessionCreator: SessionCreator,
+ private val pendingSessionStore: PendingSessionStore
+) : LoginWizard {
+
+ private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
+
+ private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
+ .create(AuthAPI::class.java)
+
+ override fun login(login: String,
+ password: String,
+ deviceName: String,
+ callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ loginInternal(login, password, deviceName)
+ }
+ }
+
+ private suspend fun loginInternal(login: String,
+ password: String,
+ deviceName: String) = withContext(coroutineDispatchers.computation) {
+ val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
+ PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
+ } else {
+ PasswordLoginParams.userIdentifier(login, password, deviceName)
+ }
+ val credentials = executeRequest {
+ apiCall = authAPI.login(loginParams)
+ }
+
+ sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+ }
+
+ override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ resetPasswordInternal(email, newPassword)
+ }
+ }
+
+ private suspend fun resetPasswordInternal(email: String, newPassword: String) {
+ val param = RegisterAddThreePidTask.Params(
+ RegisterThreePid.Email(email),
+ pendingSessionData.clientSecret,
+ pendingSessionData.sendAttempt
+ )
+
+ pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
+ .also { pendingSessionStore.savePendingSessionData(it) }
+
+ val result = executeRequest {
+ apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
+ }
+
+ pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result))
+ .also { pendingSessionStore.savePendingSessionData(it) }
+ }
+
+ override fun resetPasswordMailConfirmed(callback: MatrixCallback): Cancelable {
+ val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run {
+ callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
+ return NoOpCancellable
+ }
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ resetPasswordMailConfirmedInternal(safeResetPasswordData)
+ }
+ }
+
+ private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) {
+ val param = ResetPasswordMailConfirmed.create(
+ pendingSessionData.clientSecret,
+ resetPasswordData.addThreePidRegistrationResponse.sid,
+ resetPasswordData.newPassword
+ )
+
+ executeRequest {
+ apiCall = authAPI.resetPasswordMailConfirmed(param)
+ }
+
+ // Set to null?
+ // resetPasswordData = null
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordData.kt
new file mode 100644
index 0000000000..11a8b95443
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordData.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.matrix.android.internal.auth.login
+
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
+
+/**
+ * Container to store the data when a reset password is in the email validation step
+ */
+@JsonClass(generateAdapter = true)
+internal data class ResetPasswordData(
+ val newPassword: String,
+ val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordMailConfirmed.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordMailConfirmed.kt
new file mode 100644
index 0000000000..9be4451628
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/ResetPasswordMailConfirmed.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014 OpenMarket Ltd
+ * Copyright 2017 Vector Creations Ltd
+ * Copyright 2018 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.login
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.auth.registration.AuthParams
+
+/**
+ * Class to pass parameters to reset the password once a email has been validated.
+ */
+@JsonClass(generateAdapter = true)
+internal data class ResetPasswordMailConfirmed(
+ // authentication parameters
+ @Json(name = "auth")
+ val auth: AuthParams? = null,
+
+ // the new password
+ @Json(name = "new_password")
+ val newPassword: String? = null
+) {
+ companion object {
+ fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
+ return ResetPasswordMailConfirmed(
+ auth = AuthParams.createForResetPassword(clientSecret, sid),
+ newPassword = newPassword
+ )
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationParams.kt
new file mode 100644
index 0000000000..90e1894bac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationParams.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+
+/**
+ * Add a three Pid during authentication
+ */
+@JsonClass(generateAdapter = true)
+internal data class AddThreePidRegistrationParams(
+ /**
+ * Required. A unique string generated by the client, and used to identify the validation attempt.
+ * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed 255 characters and it must not be empty.
+ */
+ @Json(name = "client_secret")
+ val clientSecret: String,
+
+ /**
+ * Required. The server will only send an email if the send_attempt is a number greater than the most recent one which it has seen,
+ * scoped to that email + client_secret pair. This is to avoid repeatedly sending the same email in the case of request retries between
+ * the POSTing user and the identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
+ * If they do not, the server should respond with success but not resend the email.
+ */
+ @Json(name = "send_attempt")
+ val sendAttempt: Int,
+
+ /**
+ * Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when
+ * submitting 3PID validation information through a POST request.
+ */
+ @Json(name = "next_link")
+ val nextLink: String? = null,
+
+ /**
+ * Required. The hostname of the identity server to communicate with. May optionally include a port.
+ * This parameter is ignored when the homeserver handles 3PID verification.
+ */
+ @Json(name = "id_server")
+ val id_server: String? = null,
+
+ /* ==========================================================================================
+ * For emails
+ * ========================================================================================== */
+
+ /**
+ * Required. The email address to validate.
+ */
+ @Json(name = "email")
+ val email: String? = null,
+
+ /* ==========================================================================================
+ * For Msisdn
+ * ========================================================================================== */
+
+ /**
+ * Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from.
+ */
+ @Json(name = "country")
+ val countryCode: String? = null,
+
+ /**
+ * Required. The phone number to validate.
+ */
+ @Json(name = "phone_number")
+ val msisdn: String? = null
+) {
+ companion object {
+ fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams {
+ return when (params.threePid) {
+ is RegisterThreePid.Email -> AddThreePidRegistrationParams(
+ email = params.threePid.email,
+ clientSecret = params.clientSecret,
+ sendAttempt = params.sendAttempt
+ )
+ is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams(
+ msisdn = params.threePid.msisdn,
+ countryCode = params.threePid.countryCode,
+ clientSecret = params.clientSecret,
+ sendAttempt = params.sendAttempt
+ )
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationResponse.kt
new file mode 100644
index 0000000000..f07e66a7ef
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AddThreePidRegistrationResponse.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class AddThreePidRegistrationResponse(
+ /**
+ * Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
+ * Their length must not exceed 255 characters and they must not be empty.
+ */
+ @Json(name = "sid")
+ val sid: String,
+
+ /**
+ * An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
+ * Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable),
+ * who should then be prompted to provide it to the client.
+ *
+ * If this field is not present, the client can assume that verification will happen without the client's involvement provided
+ * the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
+ */
+ @Json(name = "submit_url")
+ val submitUrl: String? = null,
+
+ /* ==========================================================================================
+ * It seems that the homeserver is sending more data, we may need it
+ * ========================================================================================== */
+
+ @Json(name = "msisdn")
+ val msisdn: String? = null,
+
+ @Json(name = "intl_fmt")
+ val formattedMsisdn: String? = null,
+
+ @Json(name = "success")
+ val success: Boolean? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt
new file mode 100644
index 0000000000..ad85579550
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018 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.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
+
+/**
+ * Open class, parent to all possible authentication parameters
+ */
+@JsonClass(generateAdapter = true)
+internal data class AuthParams(
+ @Json(name = "type")
+ val type: String,
+
+ /**
+ * Note: session can be null for reset password request
+ */
+ @Json(name = "session")
+ val session: String?,
+
+ /**
+ * parameter for "m.login.recaptcha" type
+ */
+ @Json(name = "response")
+ val captchaResponse: String? = null,
+
+ /**
+ * parameter for "m.login.email.identity" type
+ */
+ @Json(name = "threepid_creds")
+ val threePidCredentials: ThreePidCredentials? = null
+) {
+
+ companion object {
+ fun createForCaptcha(session: String, captchaResponse: String): AuthParams {
+ return AuthParams(
+ type = LoginFlowTypes.RECAPTCHA,
+ session = session,
+ captchaResponse = captchaResponse
+ )
+ }
+
+ fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams {
+ return AuthParams(
+ type = LoginFlowTypes.EMAIL_IDENTITY,
+ session = session,
+ threePidCredentials = threePidCredentials
+ )
+ }
+
+ /**
+ * Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN,
+ * the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401.
+ */
+ fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams {
+ return AuthParams(
+ type = LoginFlowTypes.MSISDN,
+ session = session,
+ threePidCredentials = threePidCredentials
+ )
+ }
+
+ fun createForResetPassword(clientSecret: String, sid: String): AuthParams {
+ return AuthParams(
+ type = LoginFlowTypes.EMAIL_IDENTITY,
+ session = null,
+ threePidCredentials = ThreePidCredentials(
+ clientSecret = clientSecret,
+ sid = sid
+ )
+ )
+ }
+ }
+}
+
+@JsonClass(generateAdapter = true)
+data class ThreePidCredentials(
+ @Json(name = "client_secret")
+ val clientSecret: String? = null,
+
+ @Json(name = "id_server")
+ val idServer: String? = null,
+
+ @Json(name = "sid")
+ val sid: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt
new file mode 100644
index 0000000000..29970b6c0c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2018 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.registration
+
+import dagger.Lazy
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+import im.vector.matrix.android.api.auth.registration.RegistrationResult
+import im.vector.matrix.android.api.auth.registration.RegistrationWizard
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.Failure.RegistrationFlowError
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.api.util.NoOpCancellable
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.auth.PendingSessionStore
+import im.vector.matrix.android.internal.auth.SessionCreator
+import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
+import im.vector.matrix.android.internal.auth.db.PendingSessionData
+import im.vector.matrix.android.internal.network.RetrofitFactory
+import im.vector.matrix.android.internal.task.launchToCallback
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import okhttp3.OkHttpClient
+
+/**
+ * This class execute the registration request and is responsible to keep the session of interactive authentication
+ */
+internal class DefaultRegistrationWizard(
+ private val okHttpClient: Lazy,
+ private val retrofitFactory: RetrofitFactory,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val sessionCreator: SessionCreator,
+ private val pendingSessionStore: PendingSessionStore
+) : RegistrationWizard {
+
+ private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
+
+ private val authAPI = buildAuthAPI()
+ private val registerTask = DefaultRegisterTask(authAPI)
+ private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI)
+ private val validateCodeTask = DefaultValidateCodeTask(authAPI)
+
+ override val currentThreePid: String?
+ get() {
+ return when (val threePid = pendingSessionData.currentThreePidData?.threePid) {
+ is RegisterThreePid.Email -> threePid.email
+ is RegisterThreePid.Msisdn -> {
+ // Take formatted msisdn if provided by the server
+ pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn
+ }
+ null -> null
+ }
+ }
+
+ override val isRegistrationStarted: Boolean
+ get() = pendingSessionData.isRegistrationStarted
+
+ override fun getRegistrationFlow(callback: MatrixCallback): Cancelable {
+ val params = RegistrationParams()
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ performRegistrationRequest(params)
+ }
+ }
+
+ override fun createAccount(userName: String,
+ password: String,
+ initialDeviceDisplayName: String?,
+ callback: MatrixCallback): Cancelable {
+ val params = RegistrationParams(
+ username = userName,
+ password = password,
+ initialDeviceDisplayName = initialDeviceDisplayName
+ )
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ performRegistrationRequest(params)
+ .also {
+ pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
+ .also { pendingSessionStore.savePendingSessionData(it) }
+ }
+ }
+ }
+
+ override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable {
+ val safeSession = pendingSessionData.currentSession ?: run {
+ callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
+ return NoOpCancellable
+ }
+ val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ performRegistrationRequest(params)
+ }
+ }
+
+ override fun acceptTerms(callback: MatrixCallback): Cancelable {
+ val safeSession = pendingSessionData.currentSession ?: run {
+ callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
+ return NoOpCancellable
+ }
+ val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ performRegistrationRequest(params)
+ }
+ }
+
+ override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
+ .also { pendingSessionStore.savePendingSessionData(it) }
+
+ sendThreePid(threePid)
+ }
+ }
+
+ override fun sendAgainThreePid(callback: MatrixCallback): Cancelable {
+ val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run {
+ callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
+ return NoOpCancellable
+ }
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ sendThreePid(safeCurrentThreePid)
+ }
+ }
+
+ private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult {
+ val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first")
+ val response = registerAddThreePidTask.execute(
+ RegisterAddThreePidTask.Params(
+ threePid,
+ pendingSessionData.clientSecret,
+ pendingSessionData.sendAttempt))
+
+ pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
+ .also { pendingSessionStore.savePendingSessionData(it) }
+
+ val params = RegistrationParams(
+ auth = if (threePid is RegisterThreePid.Email) {
+ AuthParams.createForEmailIdentity(safeSession,
+ ThreePidCredentials(
+ clientSecret = pendingSessionData.clientSecret,
+ sid = response.sid
+ )
+ )
+ } else {
+ AuthParams.createForMsisdnIdentity(safeSession,
+ ThreePidCredentials(
+ clientSecret = pendingSessionData.clientSecret,
+ sid = response.sid
+ )
+ )
+ }
+ )
+ // Store data
+ pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
+ .also { pendingSessionStore.savePendingSessionData(it) }
+
+ // and send the sid a first time
+ return performRegistrationRequest(params)
+ }
+
+ override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback): Cancelable {
+ val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run {
+ callback.onFailure(IllegalStateException("developer error, no pending three pid"))
+ return NoOpCancellable
+ }
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ performRegistrationRequest(safeParam, delayMillis)
+ }
+ }
+
+ override fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ validateThreePid(code)
+ }
+ }
+
+ private suspend fun validateThreePid(code: String): RegistrationResult {
+ val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
+ ?: throw IllegalStateException("developer error, no pending three pid")
+ val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
+ val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
+ val validationBody = ValidationCodeBody(
+ clientSecret = pendingSessionData.clientSecret,
+ sid = safeCurrentData.addThreePidRegistrationResponse.sid,
+ code = code
+ )
+ val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
+ if (validationResponse.success == true) {
+ // The entered code is correct
+ // Same than validate email
+ return performRegistrationRequest(registrationParams, 3_000)
+ } else {
+ // The code is not correct
+ throw Failure.SuccessError
+ }
+ }
+
+ override fun dummy(callback: MatrixCallback): Cancelable {
+ val safeSession = pendingSessionData.currentSession ?: run {
+ callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
+ return NoOpCancellable
+ }
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
+ performRegistrationRequest(params)
+ }
+ }
+
+ private suspend fun performRegistrationRequest(registrationParams: RegistrationParams,
+ delayMillis: Long = 0): RegistrationResult {
+ delay(delayMillis)
+ val credentials = try {
+ registerTask.execute(RegisterTask.Params(registrationParams))
+ } catch (exception: Throwable) {
+ if (exception is RegistrationFlowError) {
+ pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session)
+ .also { pendingSessionStore.savePendingSessionData(it) }
+ return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult())
+ } else {
+ throw exception
+ }
+ }
+
+ val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+ return RegistrationResult.Success(session)
+ }
+
+ private fun buildAuthAPI(): AuthAPI {
+ val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
+ return retrofit.create(AuthAPI::class.java)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt
new file mode 100644
index 0000000000..2cd52f702e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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 org.matrix.androidsdk.rest.model.login
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+/**
+ * This class represent a localized privacy policy for registration Flow.
+ */
+@Parcelize
+data class LocalizedFlowDataLoginTerms(
+ var policyName: String? = null,
+ var version: String? = null,
+ var localizedUrl: String? = null,
+ var localizedName: String? = null
+) : Parcelable
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt
new file mode 100644
index 0000000000..0246075153
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterAddThreePidTask.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+
+internal interface RegisterAddThreePidTask : Task {
+ data class Params(
+ val threePid: RegisterThreePid,
+ val clientSecret: String,
+ val sendAttempt: Int
+ )
+}
+
+internal class DefaultRegisterAddThreePidTask(private val authAPI: AuthAPI)
+ : RegisterAddThreePidTask {
+
+ override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse {
+ return executeRequest {
+ apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params))
+ }
+ }
+
+ private fun RegisterThreePid.toPath(): String {
+ return when (this) {
+ is RegisterThreePid.Email -> "email"
+ is RegisterThreePid.Msisdn -> "msisdn"
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt
new file mode 100644
index 0000000000..f80021fff5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegisterTask.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.di.MoshiProvider
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+
+internal interface RegisterTask : Task {
+ data class Params(
+ val registrationParams: RegistrationParams
+ )
+}
+
+internal class DefaultRegisterTask(private val authAPI: AuthAPI)
+ : RegisterTask {
+
+ override suspend fun execute(params: RegisterTask.Params): Credentials {
+ try {
+ return executeRequest {
+ apiCall = authAPI.register(params.registrationParams)
+ }
+ } catch (throwable: Throwable) {
+ if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
+ // Parse to get a RegistrationFlowResponse
+ val registrationFlowResponse = try {
+ MoshiProvider.providesMoshi()
+ .adapter(RegistrationFlowResponse::class.java)
+ .fromJson(throwable.errorBody)
+ } catch (e: Exception) {
+ null
+ }
+ // check if the server response can be cast
+ if (registrationFlowResponse != null) {
+ throw Failure.RegistrationFlowError(registrationFlowResponse)
+ } else {
+ throw throwable
+ }
+ } else {
+ // Other error
+ throw throwable
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt
index 218251cfe5..2d3d25e538 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt
@@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.auth.registration.FlowResult
+import im.vector.matrix.android.api.auth.registration.Stage
+import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
+import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
data class RegistrationFlowResponse(
@@ -50,4 +54,46 @@ data class RegistrationFlowResponse(
*/
@Json(name = "params")
var params: JsonDict? = null
+
+ /**
+ * WARNING,
+ * The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage,
+ * But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure]
+ * Ex: when polling for "m.login.msisdn" validation
+ */
)
+
+/**
+ * Convert to something easier to handle on client side
+ */
+fun RegistrationFlowResponse.toFlowResult(): FlowResult {
+ // Get all the returned stages
+ val allFlowTypes = mutableSetOf()
+
+ val missingStage = mutableListOf()
+ val completedStage = mutableListOf()
+
+ this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } }
+
+ allFlowTypes.forEach { type ->
+ val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true
+
+ val stage = when (type) {
+ LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String)
+ ?: "")
+ LoginFlowTypes.DUMMY -> Stage.Dummy(isMandatory)
+ LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap())
+ LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory)
+ LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory)
+ else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>))
+ }
+
+ if (type in completedStages ?: emptyList()) {
+ completedStage.add(stage)
+ } else {
+ missingStage.add(stage)
+ }
+ }
+
+ return FlowResult(missingStage, completedStage)
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt
new file mode 100644
index 0000000000..6a874c7387
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationParams.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 OpenMarket Ltd
+ * Copyright 2017 Vector Creations Ltd
+ * Copyright 2018 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.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * Class to pass parameters to the different registration types for /register.
+ */
+@JsonClass(generateAdapter = true)
+internal data class RegistrationParams(
+ // authentication parameters
+ @Json(name = "auth")
+ val auth: AuthParams? = null,
+
+ // the account username
+ @Json(name = "username")
+ val username: String? = null,
+
+ // the account password
+ @Json(name = "password")
+ val password: String? = null,
+
+ // device name
+ @Json(name = "initial_device_display_name")
+ val initialDeviceDisplayName: String? = null,
+
+ // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app
+ // versions to end up in fallback because the HS returns the msisdn flow which they don't support
+ val x_show_msisdn: Boolean? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt
new file mode 100644
index 0000000000..8bfa3dda1d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class SuccessResult(
+ @Json(name = "success")
+ val success: Boolean?
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ThreePidData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ThreePidData.kt
new file mode 100644
index 0000000000..bb4751c438
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ThreePidData.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+
+/**
+ * Container to store the data when a three pid is in validation step
+ */
+@JsonClass(generateAdapter = true)
+internal data class ThreePidData(
+ val email: String,
+ val msisdn: String,
+ val country: String,
+ val addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
+ val registrationParams: RegistrationParams
+) {
+ val threePid: RegisterThreePid
+ get() {
+ return if (email.isNotBlank()) {
+ RegisterThreePid.Email(email)
+ } else {
+ RegisterThreePid.Msisdn(msisdn, country)
+ }
+ }
+
+ companion object {
+ fun from(threePid: RegisterThreePid,
+ addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
+ registrationParams: RegistrationParams): ThreePidData {
+ return when (threePid) {
+ is RegisterThreePid.Email ->
+ ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams)
+ is RegisterThreePid.Msisdn ->
+ ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams)
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt
new file mode 100644
index 0000000000..da75b839a6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidateCodeTask.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import im.vector.matrix.android.internal.auth.AuthAPI
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+
+internal interface ValidateCodeTask : Task {
+ data class Params(
+ val url: String,
+ val body: ValidationCodeBody
+ )
+}
+
+internal class DefaultValidateCodeTask(private val authAPI: AuthAPI)
+ : ValidateCodeTask {
+
+ override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult {
+ return executeRequest {
+ apiCall = authAPI.validate3Pid(params.url, params.body)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidationCodeBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidationCodeBody.kt
new file mode 100644
index 0000000000..cb3b7e5e85
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/ValidationCodeBody.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.matrix.android.internal.auth.registration
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * This object is used to send a code received by SMS to validate Msisdn ownership
+ */
+@JsonClass(generateAdapter = true)
+data class ValidationCodeBody(
+ @Json(name = "client_secret")
+ val clientSecret: String,
+
+ @Json(name = "sid")
+ val sid: String,
+
+ @Json(name = "token")
+ val code: String
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt
index f7314fe6b4..e8fa659d8d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt
@@ -22,7 +22,7 @@ import com.squareup.moshi.Moshi
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.Matrix
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore
@@ -44,7 +44,7 @@ internal interface MatrixComponent {
@Unauthenticated
fun okHttpClient(): OkHttpClient
- fun authenticator(): Authenticator
+ fun authenticationService(): AuthenticationService
fun context(): Context
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt
index d0d8d134cb..c6c10d9a8f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
+ const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
index 8a3bc1c046..51c02456d7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
@@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi
import im.vector.matrix.android.internal.session.filter.FilterRepository
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
+import im.vector.matrix.android.internal.session.user.UserStore
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
@@ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore,
- private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
+ private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
+ private val userStore: UserStore
) : SyncTask {
override suspend fun execute(params: SyncTask.Params) {
@@ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
val isInitialSync = token == null
if (isInitialSync) {
+ // We might want to get the user information in parallel too
+ userStore.createOrUpdate(userId)
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt
index 51c296ba6e..22d012269b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt
@@ -53,4 +53,7 @@ internal abstract class UserModule {
@Binds
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
+
+ @Binds
+ abstract fun bindUserStore(userStore: RealmUserStore): UserStore
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserStore.kt
new file mode 100644
index 0000000000..cf5d2a7ce4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserStore.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.matrix.android.internal.session.user
+
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.internal.database.model.UserEntity
+import im.vector.matrix.android.internal.util.awaitTransaction
+import javax.inject.Inject
+
+internal interface UserStore {
+ suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null)
+}
+
+internal class RealmUserStore @Inject constructor(private val monarchy: Monarchy) : UserStore {
+
+ override suspend fun createOrUpdate(userId: String, displayName: String?, avatarUrl: String?) {
+ monarchy.awaitTransaction {
+ val userEntity = UserEntity(userId, displayName ?: "", avatarUrl ?: "")
+ it.insertOrUpdate(userEntity)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt
new file mode 100644
index 0000000000..54c19bd86f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.matrix.android.internal.task
+
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.extensions.foldToCallback
+import im.vector.matrix.android.internal.util.toCancelable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+internal fun CoroutineScope.launchToCallback(
+ context: CoroutineContext = EmptyCoroutineContext,
+ callback: MatrixCallback,
+ block: suspend () -> T
+): Cancelable = launch(context, CoroutineStart.DEFAULT) {
+ val result = runCatching {
+ block()
+ }
+ result.foldToCallback(callback)
+}.toCancelable()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
index 14e546e0d6..d5392779d1 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt
@@ -20,8 +20,8 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
-import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
@@ -34,27 +34,28 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
private val executorScope = CoroutineScope(SupervisorJob())
fun execute(task: ConfigurableTask): Cancelable {
- val job = executorScope.launch(task.callbackThread.toDispatcher()) {
- val resultOrFailure = runCatching {
- withContext(task.executionThread.toDispatcher()) {
- Timber.v("Enqueue task $task")
- retry(task.retryCount) {
- if (task.constraints.connectedToNetwork) {
- Timber.v("Waiting network for $task")
- networkConnectivityChecker.waitUntilConnected()
+ return executorScope
+ .launch(task.callbackThread.toDispatcher()) {
+ val resultOrFailure = runCatching {
+ withContext(task.executionThread.toDispatcher()) {
+ Timber.v("Enqueue task $task")
+ retry(task.retryCount) {
+ if (task.constraints.connectedToNetwork) {
+ Timber.v("Waiting network for $task")
+ networkConnectivityChecker.waitUntilConnected()
+ }
+ Timber.v("Execute task $task on ${Thread.currentThread().name}")
+ task.execute(task.params)
+ }
}
- Timber.v("Execute task $task on ${Thread.currentThread().name}")
- task.execute(task.params)
}
+ resultOrFailure
+ .onFailure {
+ Timber.d(it, "Task failed")
+ }
+ .foldToCallback(task.callback)
}
- }
- resultOrFailure
- .onFailure {
- Timber.d(it, "Task failed")
- }
- .foldToCallback(task.callback)
- }
- return CancelableCoroutine(job)
+ .toCancelable()
}
fun cancelAll() = executorScope.coroutineContext.cancelChildren()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt
index 71e2d3fdb2..53bec0d621 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt
@@ -19,7 +19,14 @@ package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.util.Cancelable
import kotlinx.coroutines.Job
-internal class CancelableCoroutine(private val job: Job) : Cancelable {
+internal fun Job.toCancelable(): Cancelable {
+ return CancelableCoroutine(this)
+}
+
+/**
+ * Private, use the extension above
+ */
+private class CancelableCoroutine(private val job: Job) : Cancelable {
override fun cancel() {
if (!job.isCancelled) {
diff --git a/vector/build.gradle b/vector/build.gradle
index 1e19fd4d35..d77f669215 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -225,6 +225,7 @@ dependencies {
def glide_version = '4.10.0'
def moshi_version = '1.8.0'
def daggerVersion = '2.24'
+ def autofill_version = "1.0.0-rc01"
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
@@ -256,6 +257,9 @@ dependencies {
// Debug
implementation 'com.facebook.stetho:stetho:1.5.1'
+ // Phone number https://github.com/google/libphonenumber
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
+
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
@@ -290,6 +294,7 @@ dependencies {
implementation "io.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
+ implementation "androidx.autofill:autofill:$autofill_version"
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.7'
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 0c9bac61a1..5f1687c9c9 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -33,7 +33,9 @@
-
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/assets/sendObject.js b/vector/src/main/assets/sendObject.js
new file mode 100644
index 0000000000..ebde72b58d
--- /dev/null
+++ b/vector/src/main/assets/sendObject.js
@@ -0,0 +1 @@
+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;};
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
index 20a17e55d4..5ca888fc2e 100644
--- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
@@ -36,7 +36,7 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector
@@ -63,7 +63,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
lateinit var appContext: Context
// font thread handler
- @Inject lateinit var authenticator: Authenticator
+ @Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@@ -115,8 +115,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest)
notificationUtils.createNotificationChannels()
- if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
- val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
+ if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
+ val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
index 3eccb668ea..12dfcbcaac 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
@@ -17,7 +17,7 @@
package im.vector.riotx.core.di
import arrow.core.Option
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
@@ -27,7 +27,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
+class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler,
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
@@ -64,7 +64,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
// TODO: Stop sync ?
// fun switchToSession(sessionParams: SessionParams) {
-// val newActiveSession = authenticator.getSession(sessionParams)
+// val newActiveSession = authenticationService.getSession(sessionParams)
// activeSession.set(newActiveSession)
// }
}
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 6ae4619033..208246aa68 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
@@ -35,8 +35,8 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment
-import im.vector.riotx.features.login.LoginFragment
-import im.vector.riotx.features.login.LoginSsoFallbackFragment
+import im.vector.riotx.features.login.*
+import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@@ -117,8 +117,63 @@ interface FragmentModule {
@Binds
@IntoMap
- @FragmentKey(LoginSsoFallbackFragment::class)
- fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment
+ @FragmentKey(LoginCaptchaFragment::class)
+ fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginTermsFragment::class)
+ fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginServerUrlFormFragment::class)
+ fun bindLoginServerUrlFormFragment(fragment: LoginServerUrlFormFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginResetPasswordMailConfirmationFragment::class)
+ fun bindLoginResetPasswordMailConfirmationFragment(fragment: LoginResetPasswordMailConfirmationFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginResetPasswordFragment::class)
+ fun bindLoginResetPasswordFragment(fragment: LoginResetPasswordFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginResetPasswordSuccessFragment::class)
+ fun bindLoginResetPasswordSuccessFragment(fragment: LoginResetPasswordSuccessFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginServerSelectionFragment::class)
+ fun bindLoginServerSelectionFragment(fragment: LoginServerSelectionFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginSignUpSignInSelectionFragment::class)
+ fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginSplashFragment::class)
+ fun bindLoginSplashFragment(fragment: LoginSplashFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginWebFragment::class)
+ fun bindLoginWebFragment(fragment: LoginWebFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginGenericTextInputFormFragment::class)
+ fun bindLoginGenericTextInputFormFragment(fragment: LoginGenericTextInputFormFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LoginWaitForEmailFragment::class)
+ fun bindLoginWaitForEmailFragment(fragment: LoginWaitForEmailFragment): Fragment
@Binds
@IntoMap
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 17622020d0..9f0f83a41f 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
@@ -32,8 +32,8 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
-import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.riotx.features.home.room.list.RoomListModule
+import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
@@ -47,7 +47,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
-import im.vector.riotx.features.settings.*
+import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.ui.UiStateRepository
diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
index d31955ce8e..c4b2c40787 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
@@ -21,13 +21,14 @@ import android.content.res.Resources
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.Matrix
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
import im.vector.riotx.core.pushers.PushersManager
+import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
@@ -69,6 +70,8 @@ interface VectorComponent {
fun resources(): Resources
+ fun assetReader(): AssetReader
+
fun dimensionConverter(): DimensionConverter
fun vectorConfiguration(): VectorConfiguration
@@ -97,7 +100,7 @@ interface VectorComponent {
fun incomingKeyRequestHandler(): KeyRequestHandler
- fun authenticator(): Authenticator
+ fun authenticationService(): AuthenticationService
fun bugReporter(): BugReporter
diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
index e3df0eb635..84441d88e1 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt
@@ -24,7 +24,7 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.Matrix
-import im.vector.matrix.android.api.auth.Authenticator
+import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator
@@ -64,8 +64,8 @@ abstract class VectorModule {
@Provides
@JvmStatic
- fun providesAuthenticator(matrix: Matrix): Authenticator {
- return matrix.authenticator()
+ fun providesAuthenticationService(matrix: Matrix): AuthenticationService {
+ return matrix.authenticationService()
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
index cc1e4dabc7..0876701504 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
@@ -31,6 +31,7 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
+import im.vector.riotx.features.login.LoginSharedActionViewModel
import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.workers.signout.SignOutViewModel
@@ -112,4 +113,9 @@ interface ViewModelModule {
@IntoMap
@ViewModelKey(RoomDirectorySharedActionViewModel::class)
fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(LoginSharedActionViewModel::class)
+ fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel
}
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 10c4fe3354..621031f166 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
@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import java.net.SocketTimeoutException
+import java.net.UnknownHostException
import javax.inject.Inject
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
@@ -34,23 +35,61 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
return when (throwable) {
null -> null
is Failure.NetworkConnection -> {
- if (throwable.ioException is SocketTimeoutException) {
- stringProvider.getString(R.string.error_network_timeout)
- } else {
- stringProvider.getString(R.string.error_no_network)
+ when {
+ throwable.ioException is SocketTimeoutException ->
+ stringProvider.getString(R.string.error_network_timeout)
+ throwable.ioException is UnknownHostException ->
+ // Invalid homeserver?
+ stringProvider.getString(R.string.login_error_unknown_host)
+ else ->
+ stringProvider.getString(R.string.error_no_network)
}
}
is Failure.ServerError -> {
- if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) {
- // Special case for terms and conditions
- stringProvider.getString(R.string.error_terms_not_accepted)
- } else {
- throwable.error.message.takeIf { it.isNotEmpty() }
- ?: throwable.error.code.takeIf { it.isNotEmpty() }
+ when {
+ throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> {
+ // Special case for terms and conditions
+ stringProvider.getString(R.string.error_terms_not_accepted)
+ }
+ throwable.error.code == MatrixError.FORBIDDEN
+ && throwable.error.message == "Invalid password" -> {
+ stringProvider.getString(R.string.auth_invalid_login_param)
+ }
+ throwable.error.code == MatrixError.USER_IN_USE -> {
+ stringProvider.getString(R.string.login_signup_error_user_in_use)
+ }
+ throwable.error.code == MatrixError.BAD_JSON -> {
+ stringProvider.getString(R.string.login_error_bad_json)
+ }
+ throwable.error.code == MatrixError.NOT_JSON -> {
+ stringProvider.getString(R.string.login_error_not_json)
+ }
+ throwable.error.code == MatrixError.LIMIT_EXCEEDED -> {
+ limitExceededError(throwable.error)
+ }
+ throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> {
+ stringProvider.getString(R.string.login_reset_password_error_not_found)
+ }
+ else -> {
+ throwable.error.message.takeIf { it.isNotEmpty() }
+ ?: throwable.error.code.takeIf { it.isNotEmpty() }
+ }
}
}
else -> throwable.localizedMessage
}
?: stringProvider.getString(R.string.unknown_error)
}
+
+ private fun limitExceededError(error: MatrixError): String {
+ val delay = error.retryAfterMillis
+
+ return if (delay == null) {
+ stringProvider.getString(R.string.login_error_limit_exceeded)
+ } else {
+ // Ensure at least 1 second
+ val delaySeconds = delay.toInt() / 1000 + 1
+ stringProvider.getQuantityString(R.plurals.login_error_limit_exceeded_retry_after, delaySeconds, delaySeconds)
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt
new file mode 100644
index 0000000000..dd4257fe1f
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.core.error
+
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
+import javax.net.ssl.HttpsURLConnection
+
+fun Throwable.is401(): Boolean {
+ return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
+ && this.error.code == MatrixError.UNAUTHORIZED)
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
index 6d7c3d39e6..f9f5d3b3d2 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
@@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions
import android.os.Parcelable
import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentTransaction
import im.vector.riotx.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
@@ -44,8 +45,13 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment,
supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
-fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
+fun VectorBaseActivity.addFragmentToBackstack(frameId: Int,
+ fragmentClass: Class,
+ params: Parcelable? = null,
+ tag: String? = null,
+ option: ((FragmentTransaction) -> Unit)? = null) {
supportFragmentManager.commitTransaction {
+ option?.invoke(this)
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
index 1e3da7f878..5bd6852e8a 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
@@ -17,6 +17,7 @@
package im.vector.riotx.core.extensions
import android.os.Bundle
+import android.util.Patterns
import androidx.fragment.app.Fragment
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@@ -27,3 +28,8 @@ inline fun T.ooi(block: (T) -> Unit): T = also(block)
* Apply argument to a Fragment
*/
fun T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
+
+/**
+ * Check if a CharSequence is an email
+ */
+fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
index 7db27ececb..b93ab3fdce 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
@@ -79,3 +79,6 @@ fun VectorBaseFragment.addChildFragmentToBackstack(frameId: Int,
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
}
}
+
+// Define a missing constant
+const val POP_BACK_STACK_EXCLUSIVE = 0
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt
index 17f7730f86..c8a58997a1 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/OnBackPressed.kt
@@ -21,6 +21,7 @@ interface OnBackPressed {
/**
* Returns true, if the on back pressed event has been handled by this Fragment.
* Otherwise return false
+ * @param toolbarButton true if this is the back button from the toolbar
*/
- fun onBackPressed(): Boolean
+ fun onBackPressed(toolbarButton: Boolean): Boolean
}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index 4a3056657f..79b040cd41 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -278,7 +278,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
- onBackPressed()
+ onBackPressed(true)
return true
}
@@ -286,20 +286,24 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
override fun onBackPressed() {
- val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
+ onBackPressed(false)
+ }
+
+ private fun onBackPressed(fromToolbar: Boolean) {
+ val handled = recursivelyDispatchOnBackPressed(supportFragmentManager, fromToolbar)
if (!handled) {
super.onBackPressed()
}
}
- private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
- val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed()
+ private fun recursivelyDispatchOnBackPressed(fm: FragmentManager, fromToolbar: Boolean): Boolean {
+ val reverseOrder = fm.fragments.filterIsInstance().reversed()
for (f in reverseOrder) {
- val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
+ val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager, fromToolbar)
if (handledByChildFragments) {
return true
}
- if (f is OnBackPressed && f.onBackPressed()) {
+ if (f is OnBackPressed && f.onBackPressed(fromToolbar)) {
return true
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt
new file mode 100644
index 0000000000..908f0e68b6
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/utils/AssetReader.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 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.core.utils
+
+import android.content.Context
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Read asset files
+ */
+class AssetReader @Inject constructor(private val context: Context) {
+
+ /* ==========================================================================================
+ * CACHE
+ * ========================================================================================== */
+ private val cache = mutableMapOf()
+
+ /**
+ * Read an asset from resource and return a String or null in case of error.
+ *
+ * @param assetFilename Asset filename
+ * @return the content of the asset file, or null in case of error
+ */
+ fun readAssetFile(assetFilename: String): String? {
+ return cache.getOrPut(assetFilename, {
+ return try {
+ context.assets.open(assetFilename)
+ .use { asset ->
+ buildString {
+ var ch = asset.read()
+ while (ch != -1) {
+ append(ch.toChar())
+ ch = asset.read()
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "## readAssetFile() failed")
+ null
+ }
+ })
+ }
+
+ fun clearCache() {
+ cache.clear()
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
new file mode 100644
index 0000000000..335b9112ef
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/utils/ViewUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.core.utils
+
+import android.text.Editable
+import android.view.ViewGroup
+import androidx.core.view.children
+import com.google.android.material.textfield.TextInputLayout
+import im.vector.riotx.core.platform.SimpleTextWatcher
+
+/**
+ * Find all TextInputLayout in a ViewGroup and in all its descendants
+ */
+fun ViewGroup.findAllTextInputLayout(): List {
+ val res = ArrayList()
+
+ children.forEach {
+ if (it is TextInputLayout) {
+ res.add(it)
+ } else if (it is ViewGroup) {
+ // Recursive call
+ res.addAll(it.findAllTextInputLayout())
+ }
+ }
+
+ return res
+}
+
+/**
+ * Add a text change listener to all TextInputEditText to reset error on its TextInputLayout when the text is changed
+ */
+fun autoResetTextInputLayoutErrors(textInputLayouts: List) {
+ textInputLayouts.forEach {
+ it.editText?.addTextChangedListener(object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ // Reset the error
+ it.error = null
+ }
+ })
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
index 02a206fc9b..7064ad0d49 100644
--- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
@@ -21,9 +21,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
-import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
-import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@@ -56,8 +54,6 @@ class MainActivity : VectorBaseActivity() {
}
}
- @Inject lateinit var matrix: Matrix
- @Inject lateinit var authenticator: Authenticator
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
index a5e9a7b4bf..04d1802264 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
@@ -329,7 +329,7 @@ class RoomListFragment @Inject constructor(
stateView.state = StateView.State.Error(message)
}
- override fun onBackPressed(): Boolean {
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (createChatFabMenu.onBackPressed()) {
return true
}
diff --git a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt
index 9535499d70..f485226935 100644
--- a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt
+++ b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt
@@ -68,8 +68,8 @@ object ServerUrlsRepository {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(HOME_SERVER_URL_PREF,
- prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
- getDefaultHomeServerUrl(context))!!)!!
+ prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
+ getDefaultHomeServerUrl(context))!!)!!
}
/**
@@ -80,5 +80,5 @@ object ServerUrlsRepository {
/**
* Return default home server url from resources
*/
- fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url)
+ fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt
new file mode 100644
index 0000000000..6cca32cf7f
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AlertDialog
+import androidx.transition.TransitionInflater
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
+import im.vector.riotx.R
+import im.vector.riotx.core.platform.OnBackPressed
+import im.vector.riotx.core.platform.VectorBaseFragment
+import javax.net.ssl.HttpsURLConnection
+
+/**
+ * Parent Fragment for all the login/registration screens
+ */
+abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
+
+ protected val loginViewModel: LoginViewModel by activityViewModel()
+ protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
+
+ private var isResetPasswordStarted = false
+
+ // Due to async, we keep a boolean to avoid displaying twice the cancellation dialog
+ private var displayCancelDialog = true
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
+ }
+ }
+
+ @CallSuper
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java)
+
+ loginViewModel.viewEvents
+ .observe()
+ .subscribe {
+ handleLoginViewEvents(it)
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
+ when (loginViewEvents) {
+ is LoginViewEvents.Error -> showError(loginViewEvents.throwable)
+ else ->
+ // This is handled by the Activity
+ Unit
+ }
+ }
+
+ private fun showError(throwable: Throwable) {
+ when (throwable) {
+ is Failure.ServerError -> {
+ if (throwable.error.code == MatrixError.FORBIDDEN
+ && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(getString(R.string.login_registration_disabled))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ } else {
+ onError(throwable)
+ }
+ }
+ else -> onError(throwable)
+ }
+ }
+
+ abstract fun onError(throwable: Throwable)
+
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ return when {
+ displayCancelDialog && loginViewModel.isRegistrationStarted -> {
+ // Ask for confirmation before cancelling the registration
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.login_signup_cancel_confirmation_title)
+ .setMessage(R.string.login_signup_cancel_confirmation_content)
+ .setPositiveButton(R.string.yes) { _, _ ->
+ displayCancelDialog = false
+ vectorBaseActivity.onBackPressed()
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+
+ true
+ }
+ displayCancelDialog && isResetPasswordStarted -> {
+ // Ask for confirmation before cancelling the reset password
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.login_reset_password_cancel_confirmation_title)
+ .setMessage(R.string.login_reset_password_cancel_confirmation_content)
+ .setPositiveButton(R.string.yes) { _, _ ->
+ displayCancelDialog = false
+ vectorBaseActivity.onBackPressed()
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+
+ true
+ }
+ else -> {
+ resetViewModel()
+ // Do not consume the Back event
+ false
+ }
+ }
+ }
+
+ final override fun invalidate() = withState(loginViewModel) { state ->
+ // True when email is sent with success to the homeserver
+ isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
+
+ updateWithState(state)
+ }
+
+ open fun updateWithState(state: LoginViewState) {
+ // No op by default
+ }
+
+ // Reset any modification on the loginViewModel by the current fragment
+ abstract fun resetViewModel()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/Config.kt b/vector/src/main/java/im/vector/riotx/features/login/Config.kt
new file mode 100644
index 0000000000..964e3fa0a1
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/Config.kt
@@ -0,0 +1,20 @@
+/*
+ * 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
+
+// TODO Check the link with Nad
+const val MODULAR_LINK = "https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication"
diff --git a/vector/src/main/java/im/vector/riotx/features/login/HomeServerConnectionConfigFactory.kt b/vector/src/main/java/im/vector/riotx/features/login/HomeServerConnectionConfigFactory.kt
new file mode 100644
index 0000000000..9f116b99f7
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/HomeServerConnectionConfigFactory.kt
@@ -0,0 +1,39 @@
+/*
+ * 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 im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import timber.log.Timber
+import javax.inject.Inject
+
+class HomeServerConnectionConfigFactory @Inject constructor() {
+
+ fun create(url: String?): HomeServerConnectionConfig? {
+ if (url == null) {
+ return null
+ }
+
+ return try {
+ HomeServerConnectionConfig.Builder()
+ .withHomeServerUri(url)
+ .build()
+ } catch (t: Throwable) {
+ Timber.e(t)
+ null
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt
new file mode 100644
index 0000000000..4d88cf6097
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/JavascriptResponse.kt
@@ -0,0 +1,39 @@
+/*
+ * 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 com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.auth.data.Credentials
+
+@JsonClass(generateAdapter = true)
+data class JavascriptResponse(
+ @Json(name = "action")
+ val action: String? = null,
+
+ /**
+ * Use for captcha result
+ */
+ @Json(name = "response")
+ val response: String? = null,
+
+ /**
+ * Used for login/registration result
+ */
+ @Json(name = "credentials")
+ val credentials: Credentials? = null
+)
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 bb42bc8e0c..618b3ea85d 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
@@ -17,12 +17,42 @@
package im.vector.riotx.features.login
import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class LoginAction : VectorViewModelAction {
+ data class UpdateServerType(val serverType: ServerType) : LoginAction()
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
- data class Login(val login: String, val password: String) : LoginAction()
- data class SsoLoginSuccess(val credentials: Credentials) : LoginAction()
- data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction()
+ data class UpdateSignMode(val signMode: SignMode) : LoginAction()
+ data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
+ data class ResetPassword(val email: String, val newPassword: String) : LoginAction()
+ object ResetPasswordMailConfirmed : LoginAction()
+
+ // Login or Register, depending on the signMode
+ data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : LoginAction()
+
+ // Register actions
+ open class RegisterAction : LoginAction()
+
+ data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction()
+ object SendAgainThreePid : RegisterAction()
+ // TODO Confirm Email (from link in the email, open in the phone, intercepted by RiotX)
+ data class ValidateThreePid(val code: String) : RegisterAction()
+
+ data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction()
+ object StopEmailValidationCheck : RegisterAction()
+
+ data class CaptchaDone(val captchaResponse: String) : RegisterAction()
+ object AcceptTerms : RegisterAction()
+ object RegisterDummy : RegisterAction()
+
+ // Reset actions
+ open class ResetAction : LoginAction()
+
+ object ResetHomeServerType : ResetAction()
+ object ResetHomeServerUrl : ResetAction()
+ object ResetSignMode : ResetAction()
+ object ResetLogin : ResetAction()
+ object ResetResetPassword : ResetAction()
}
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 abed22cb5e..2dec402f85 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,28 +18,41 @@ package im.vector.riotx.features.login
import android.content.Context
import android.content.Intent
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.ViewCompat
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
-import com.airbnb.mvrx.Success
+import androidx.fragment.app.FragmentTransaction
import com.airbnb.mvrx.viewModel
+import com.airbnb.mvrx.withState
+import im.vector.matrix.android.api.auth.registration.FlowResult
+import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE
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.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
-import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.home.HomeActivity
+import im.vector.riotx.features.login.terms.LoginTermsFragment
+import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument
+import im.vector.riotx.features.login.terms.toLocalizedLoginTerms
+import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
-class LoginActivity : VectorBaseActivity() {
-
- // Supported navigation actions for this Activity
- sealed class Navigation {
- object OpenSsoLoginFallback : Navigation()
- object GoBack : Navigation()
- }
+/**
+ * The LoginActivity manages the fragment navigation and also display the loading View
+ */
+class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private val loginViewModel: LoginViewModel by viewModel()
+ private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@@ -47,42 +60,290 @@ class LoginActivity : VectorBaseActivity() {
injector.inject(this)
}
- override fun getLayoutRes() = R.layout.activity_simple
+ private val enterAnim = R.anim.enter_fade_in
+ private val exitAnim = R.anim.exit_fade_out
+
+ private val popEnterAnim = R.anim.no_anim
+ private val popExitAnim = R.anim.exit_fade_out
+
+ private val topFragment: Fragment?
+ get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer)
+
+ private val commonOption: (FragmentTransaction) -> Unit = { ft ->
+ // Find the loginLogo on the current Fragment, this should not return null
+ (topFragment?.view as? ViewGroup)
+ // Find findViewById does not work, I do not know why
+ // findViewById(R.id.loginLogo)
+ ?.children
+ ?.first { it.id == R.id.loginLogo }
+ ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
+ // TODO
+ ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
+ }
+
+ override fun getLayoutRes() = R.layout.activity_login
override fun initUiAndData() {
if (isFirstCreation()) {
- addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java)
+ addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java)
}
// Get config extra
val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG)
if (loginConfig != null && isFirstCreation()) {
+ // TODO Check this
loginViewModel.handle(LoginAction.InitWith(loginConfig))
}
- loginViewModel.navigationLiveData.observeEvent(this) {
- when (it) {
- is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java)
- is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java)
+ loginSharedActionViewModel.observe()
+ .subscribe {
+ handleLoginNavigation(it)
+ }
+ .disposeOnDestroy()
+
+ loginViewModel
+ .subscribe(this) {
+ updateWithState(it)
+ }
+ .disposeOnDestroy()
+
+ loginViewModel.viewEvents
+ .observe()
+ .subscribe {
+ handleLoginViewEvents(it)
+ }
+ .disposeOnDestroy()
+ }
+
+ private fun handleLoginNavigation(loginNavigation: LoginNavigation) {
+ // Assigning to dummy make sure we do not forget a case
+ @Suppress("UNUSED_VARIABLE")
+ val dummy = when (loginNavigation) {
+ is LoginNavigation.OpenServerSelection ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginServerSelectionFragment::class.java,
+ option = { ft ->
+ findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
+ findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
+ findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
+ // TODO Disabled because it provokes a flickering
+ // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
+ })
+ is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone()
+ is LoginNavigation.OnSignModeSelected -> onSignModeSelected()
+ is LoginNavigation.OnLoginFlowRetrieved ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginSignUpSignInSelectionFragment::class.java,
+ option = commonOption)
+ is LoginNavigation.OnWebLoginError -> onWebLoginError(loginNavigation)
+ is LoginNavigation.OnForgetPasswordClicked ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginResetPasswordFragment::class.java,
+ option = commonOption)
+ is LoginNavigation.OnResetPasswordSendThreePidDone -> {
+ supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginResetPasswordMailConfirmationFragment::class.java,
+ option = commonOption)
}
+ is LoginNavigation.OnResetPasswordMailConfirmationSuccess -> {
+ supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginResetPasswordSuccessFragment::class.java,
+ option = commonOption)
+ }
+ is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> {
+ // Go back to the login fragment
+ supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
+ }
+ is LoginNavigation.OnSendEmailSuccess ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginWaitForEmailFragment::class.java,
+ LoginWaitForEmailFragmentArgument(loginNavigation.email),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ is LoginNavigation.OnSendMsisdnSuccess ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginGenericTextInputFormFragment::class.java,
+ LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ }
+ }
+
+ private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
+ when (loginViewEvents) {
+ is LoginViewEvents.RegistrationFlowResult -> {
+ // Check that all flows are supported by the application
+ if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) {
+ // Display a popup to propose use web fallback
+ onRegistrationStageNotSupported()
+ } else {
+ if (loginViewEvents.isRegistrationStarted) {
+ // Go on with registration flow
+ handleRegistrationNavigation(loginViewEvents.flowResult)
+ } else {
+ // First ask for login and password
+ // I add a tag to indicate that this fragment is a registration stage.
+ // This way it will be automatically popped in when starting the next registration stage
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginFragment::class.java,
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption
+ )
+ }
+ }
+ }
+ is LoginViewEvents.OutdatedHomeserver ->
+ AlertDialog.Builder(this)
+ .setTitle(R.string.login_error_outdated_homeserver_title)
+ .setMessage(R.string.login_error_outdated_homeserver_content)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ is LoginViewEvents.Error ->
+ // This is handled by the Fragments
+ Unit
+ }
+ }
+
+ private fun updateWithState(loginViewState: LoginViewState) {
+ if (loginViewState.isUserLogged()) {
+ val intent = HomeActivity.newIntent(this)
+ startActivity(intent)
+ finish()
+ return
}
- loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) {
- if (it is Success) {
- val intent = HomeActivity.newIntent(this)
- startActivity(intent)
- finish()
+ // Loading
+ loginLoading.isVisible = loginViewState.isLoading()
+ }
+
+ private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) {
+ // Pop the backstack
+ supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+
+ // And inform the user
+ AlertDialog.Builder(this)
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ private fun onServerSelectionDone() = withState(loginViewModel) { state ->
+ when (state.serverType) {
+ ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
+ ServerType.Modular,
+ ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginServerUrlFormFragment::class.java,
+ option = commonOption)
+ }
+ }
+
+ private fun onSignModeSelected() = withState(loginViewModel) { state ->
+ when (state.signMode) {
+ SignMode.Unknown -> error("Sign mode has to be set before calling this method")
+ SignMode.SignUp -> {
+ // This is managed by the LoginViewEvents
+ }
+ SignMode.SignIn -> {
+ // It depends on the LoginMode
+ when (state.loginMode) {
+ LoginMode.Unknown -> error("Developer error")
+ LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginFragment::class.java,
+ tag = FRAGMENT_LOGIN_TAG,
+ option = commonOption)
+ LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginWebFragment::class.java,
+ option = commonOption)
+ LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
+ }
}
}
}
- override fun onResume() {
- super.onResume()
+ private fun onRegistrationStageNotSupported() {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(getString(R.string.login_registration_not_supported))
+ .setPositiveButton(R.string.yes) { _, _ ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginWebFragment::class.java,
+ option = commonOption)
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ }
- showDisclaimerDialog(this)
+ private fun onLoginModeNotSupported(supportedTypes: List) {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
+ .setPositiveButton(R.string.yes) { _, _ ->
+ addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginWebFragment::class.java,
+ option = commonOption)
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ }
+
+ private fun handleRegistrationNavigation(flowResult: FlowResult) {
+ // Complete all mandatory stages first
+ val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
+
+ if (mandatoryStage != null) {
+ doStage(mandatoryStage)
+ } else {
+ // Consider optional stages
+ val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy }
+ if (optionalStage == null) {
+ // Should not happen...
+ } else {
+ doStage(optionalStage)
+ }
+ }
+ }
+
+ private fun doStage(stage: Stage) {
+ // Ensure there is no fragment for registration stage in the backstack
+ supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+
+ when (stage) {
+ is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginCaptchaFragment::class.java,
+ LoginCaptchaFragmentArgument(stage.publicKey),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginGenericTextInputFormFragment::class.java,
+ LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginGenericTextInputFormFragment::class.java,
+ LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
+ LoginTermsFragment::class.java,
+ LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
+ tag = FRAGMENT_REGISTRATION_STAGE_TAG,
+ option = commonOption)
+ else -> Unit // Should not happen
+ }
+ }
+
+ override fun configure(toolbar: Toolbar) {
+ configureToolbar(toolbar)
}
companion object {
+ private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
+ private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
+
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt
new file mode 100644
index 0000000000..3ff3e902cb
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.Parcelable
+import android.view.KeyEvent
+import android.webkit.*
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.args
+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 kotlinx.android.parcel.Parcelize
+import kotlinx.android.synthetic.main.fragment_login_captcha.*
+import timber.log.Timber
+import java.net.URLDecoder
+import java.util.*
+import javax.inject.Inject
+
+@Parcelize
+data class LoginCaptchaFragmentArgument(
+ val siteKey: String
+) : Parcelable
+
+/**
+ * In this screen, the user is asked to confirm he is not a robot
+ */
+class LoginCaptchaFragment @Inject constructor(
+ private val assetReader: AssetReader,
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_captcha
+
+ private val params: LoginCaptchaFragmentArgument by args()
+
+ private var isWebViewLoaded = false
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun setupWebView(state: LoginViewState) {
+ loginCaptchaWevView.settings.javaScriptEnabled = true
+
+ val reCaptchaPage = assetReader.readAssetFile("reCaptchaPage.html") ?: error("missing asset reCaptchaPage.html")
+
+ val html = Formatter().format(reCaptchaPage, params.siteKey).toString()
+ val mime = "text/html"
+ val encoding = "utf-8"
+
+ val homeServerUrl = state.homeServerUrl ?: error("missing url of homeserver")
+ loginCaptchaWevView.loadDataWithBaseURL(homeServerUrl, html, mime, encoding, null)
+ loginCaptchaWevView.requestLayout()
+
+ loginCaptchaWevView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+
+ // Show loader
+ loginCaptchaProgress.isVisible = true
+ }
+
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+
+ // Hide loader
+ loginCaptchaProgress.isVisible = false
+ }
+
+ override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
+ Timber.d("## onReceivedSslError() : " + error.certificate)
+
+ if (!isAdded) {
+ return
+ }
+
+ AlertDialog.Builder(requireActivity())
+ .setMessage(R.string.ssl_could_not_verify)
+ .setPositiveButton(R.string.ssl_trust) { _, _ ->
+ Timber.d("## onReceivedSslError() : the user trusted")
+ handler.proceed()
+ }
+ .setNegativeButton(R.string.ssl_do_not_trust) { _, _ ->
+ Timber.d("## onReceivedSslError() : the user did not trust")
+ handler.cancel()
+ }
+ .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
+ if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ handler.cancel()
+ Timber.d("## onReceivedSslError() : the user dismisses the trust dialog.")
+ dialog.dismiss()
+ return@OnKeyListener true
+ }
+ false
+ })
+ .setCancelable(false)
+ .show()
+ }
+
+ // common error message
+ private fun onError(errorMessage: String) {
+ Timber.e("## onError() : $errorMessage")
+
+ // TODO
+ // Toast.makeText(this@AccountCreationCaptchaActivity, errorMessage, Toast.LENGTH_LONG).show()
+
+ // on error case, close this activity
+ // runOnUiThread(Runnable { finish() })
+ }
+
+ @SuppressLint("NewApi")
+ override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
+ super.onReceivedHttpError(view, request, errorResponse)
+
+ if (request.url.toString().endsWith("favicon.ico")) {
+ // Ignore this error
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ onError(errorResponse.reasonPhrase)
+ } else {
+ onError(errorResponse.toString())
+ }
+ }
+
+ override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
+ @Suppress("DEPRECATION")
+ super.onReceivedError(view, errorCode, description, failingUrl)
+ onError(description)
+ }
+
+ override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+ if (url?.startsWith("js:") == true) {
+ var json = url.substring(3)
+ var javascriptResponse: JavascriptResponse? = null
+
+ try {
+ // URL decode
+ json = URLDecoder.decode(json, "UTF-8")
+ javascriptResponse = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java).fromJson(json)
+ } catch (e: Exception) {
+ Timber.e(e, "## shouldOverrideUrlLoading(): failed")
+ }
+
+ val response = javascriptResponse?.response
+ if (javascriptResponse?.action == "verifyCallback" && response != null) {
+ loginViewModel.handle(LoginAction.CaptchaDone(response))
+ }
+ }
+ return true
+ }
+ }
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ if (!isWebViewLoaded) {
+ setupWebView(state)
+ isWebViewLoaded = true
+ }
+ }
+}
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 456e4b2bb3..cc4f25141e 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
@@ -16,34 +16,38 @@
package im.vector.riotx.features.login
+import android.os.Build
import android.os.Bundle
import android.view.View
-import android.view.inputmethod.EditorInfo
-import android.widget.Toast
+import androidx.autofill.HintConstants
import androidx.core.view.isVisible
-import androidx.transition.TransitionManager
-import com.airbnb.mvrx.*
-import com.jakewharton.rxbinding3.view.focusChanges
+import butterknife.OnClick
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
-import im.vector.riotx.core.extensions.setTextWithColoredPart
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.extensions.hideKeyboard
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.homeserver.ServerUrlsRepository
import io.reactivex.Observable
-import io.reactivex.functions.Function3
+import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_login.*
import javax.inject.Inject
/**
- * What can be improved:
- * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
+ * In this screen, in signin mode:
+ * - the user is asked for login and password to sign in to a homeserver.
+ * - He also can reset his password
+ * In signup mode:
+ * - the user is asked for login and password
*/
-class LoginFragment @Inject constructor() : VectorBaseFragment() {
-
- private val viewModel: LoginViewModel by activityViewModel()
+class LoginFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
private var passwordShown = false
@@ -52,69 +56,101 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- setupNotice()
- setupAuthButton()
+ setupSubmitButton()
setupPasswordReveal()
+ }
- homeServerField.focusChanges()
- .subscribe {
- if (!it) {
- viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString()))
- }
+ private fun setupAutoFill(state: LoginViewState) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ when (state.signMode) {
+ SignMode.Unknown -> error("developer error")
+ SignMode.SignUp -> {
+ loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
+ passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
+ }
+ SignMode.SignIn -> {
+ loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
+ passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
}
- .disposeOnDestroyView()
-
- homeServerField.setOnEditorActionListener { _, actionId, _ ->
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString()))
- return@setOnEditorActionListener true
}
- return@setOnEditorActionListener false
- }
-
- val initHsUrl = viewModel.getInitialHomeServerUrl()
- if (initHsUrl != null) {
- homeServerField.setText(initHsUrl)
- } else {
- homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext()))
- }
- viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString()))
- }
-
- private fun setupNotice() {
- riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
-
- riotx_no_registration_notice.setOnClickListener {
- openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads")
}
}
- private fun authenticate() {
- val login = loginField.text?.trim().toString()
- val password = passwordField.text?.trim().toString()
+ @OnClick(R.id.loginSubmit)
+ fun submit() {
+ cleanupUi()
- viewModel.handle(LoginAction.Login(login, password))
+ val login = loginField.text.toString()
+ val password = passwordField.text.toString()
+
+ loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device)))
}
- private fun setupAuthButton() {
+ private fun cleanupUi() {
+ loginSubmit.hideKeyboard()
+ loginFieldTil.error = null
+ passwordFieldTil.error = null
+ }
+
+ private fun setupUi(state: LoginViewState) {
+ val resId = when (state.signMode) {
+ SignMode.Unknown -> error("developer error")
+ SignMode.SignUp -> R.string.login_signup_to
+ SignMode.SignIn -> R.string.login_connect_to
+ }
+
+ when (state.serverType) {
+ ServerType.MatrixOrg -> {
+ loginServerIcon.isVisible = true
+ loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
+ loginTitle.text = getString(resId, state.homeServerUrlSimple)
+ loginNotice.text = getString(R.string.login_server_matrix_org_text)
+ }
+ ServerType.Modular -> {
+ loginServerIcon.isVisible = true
+ loginServerIcon.setImageResource(R.drawable.ic_logo_modular)
+ // TODO
+ loginTitle.text = getString(resId, "TODO")
+ loginNotice.text = getString(R.string.login_server_modular_text)
+ }
+ ServerType.Other -> {
+ loginServerIcon.isVisible = false
+ loginTitle.text = getString(resId, state.homeServerUrlSimple)
+ loginNotice.text = getString(R.string.login_server_other_text)
+ }
+ }
+ }
+
+ private fun setupButtons(state: LoginViewState) {
+ forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn
+
+ loginSubmit.text = getString(when (state.signMode) {
+ SignMode.Unknown -> error("developer error")
+ SignMode.SignUp -> R.string.login_signup_submit
+ SignMode.SignIn -> R.string.login_signin
+ })
+ }
+
+ private fun setupSubmitButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
- homeServerField.textChanges().map { it.trim().isNotEmpty() },
- Function3 { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
- isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
+ BiFunction { isLoginNotEmpty, isPasswordNotEmpty ->
+ isLoginNotEmpty && isPasswordNotEmpty
}
)
- .subscribeBy { authenticateButton.isEnabled = it }
+ .subscribeBy {
+ loginFieldTil.error = null
+ passwordFieldTil.error = null
+ loginSubmit.isEnabled = it
+ }
.disposeOnDestroyView()
- authenticateButton.setOnClickListener { authenticate() }
-
- authenticateButtonSso.setOnClickListener { openSso() }
}
- private fun openSso() {
- viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback))
+ @OnClick(R.id.forgetPasswordButton)
+ fun forgetPasswordClicked() {
+ loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked)
}
private fun setupPasswordReveal() {
@@ -141,73 +177,47 @@ class LoginFragment @Inject constructor() : VectorBaseFragment() {
}
}
- override fun invalidate() = withState(viewModel) { state ->
- TransitionManager.beginDelayedTransition(login_fragment)
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
- when (state.asyncHomeServerLoginFlowRequest) {
- 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 -> {
- 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 -> {
- progressBar.isVisible = false
- touchArea.isVisible = false
+ override fun onError(throwable: Throwable) {
+ loginFieldTil.error = errorFormatter.toHumanReadable(throwable)
+ }
- when (state.asyncHomeServerLoginFlowRequest()) {
- LoginMode.Password -> {
- loginField.isVisible = true
- passwordContainer.isVisible = true
- authenticateButton.isVisible = true
- authenticateButtonSso.isVisible = false
- if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) {
- // Jump focus to login
- loginField.requestFocus()
- }
- }
- 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()
- }
- }
- }
- }
+ override fun updateWithState(state: LoginViewState) {
+ setupUi(state)
+ setupAutoFill(state)
+ setupButtons(state)
when (state.asyncLoginAction) {
is Loading -> {
- progressBar.isVisible = true
- touchArea.isVisible = true
-
+ // Ensure password is hidden
passwordShown = false
renderPasswordField()
}
is Fail -> {
- progressBar.isVisible = false
- touchArea.isVisible = false
- Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
+ val error = state.asyncLoginAction.error
+ if (error is Failure.ServerError
+ && error.error.code == MatrixError.FORBIDDEN
+ && error.error.message.isEmpty()) {
+ // Login with email, but email unknown
+ loginFieldTil.error = getString(R.string.login_login_with_email_error)
+ } else {
+ // Trick to display the error without text.
+ loginFieldTil.error = " "
+ passwordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error)
+ }
+ }
+ // Success is handled by the LoginActivity
+ is Success -> Unit
+ }
+
+ when (state.asyncRegistration) {
+ is Loading -> {
+ // Ensure password is hidden
+ passwordShown = false
+ renderPasswordField()
}
// Success is handled by the LoginActivity
is Success -> Unit
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt
new file mode 100644
index 0000000000..527b0c6802
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.text.InputType
+import android.view.View
+import androidx.autofill.HintConstants
+import androidx.core.view.isVisible
+import butterknife.OnClick
+import com.airbnb.mvrx.args
+import com.google.i18n.phonenumbers.NumberParseException
+import com.google.i18n.phonenumbers.PhoneNumberUtil
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.matrix.android.api.auth.registration.RegisterThreePid
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.error.is401
+import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.extensions.isEmail
+import im.vector.riotx.core.extensions.setTextOrHide
+import kotlinx.android.parcel.Parcelize
+import kotlinx.android.synthetic.main.fragment_login_generic_text_input_form.*
+import javax.inject.Inject
+
+enum class TextInputFormFragmentMode {
+ SetEmail,
+ SetMsisdn,
+ ConfirmMsisdn
+}
+
+@Parcelize
+data class LoginGenericTextInputFormFragmentArgument(
+ val mode: TextInputFormFragmentMode,
+ val mandatory: Boolean,
+ val extra: String = ""
+) : Parcelable
+
+/**
+ * In this screen, the user is asked for a text input
+ */
+class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() {
+
+ private val params: LoginGenericTextInputFormFragmentArgument by args()
+
+ override fun getLayoutResId() = R.layout.fragment_login_generic_text_input_form
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupUi()
+ setupSubmitButton()
+ setupTil()
+ setupAutoFill()
+ }
+
+ private fun setupAutoFill() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ loginGenericTextInputFormTextInput.setAutofillHints(
+ when (params.mode) {
+ TextInputFormFragmentMode.SetEmail -> HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS
+ TextInputFormFragmentMode.SetMsisdn -> HintConstants.AUTOFILL_HINT_PHONE_NUMBER
+ TextInputFormFragmentMode.ConfirmMsisdn -> HintConstants.AUTOFILL_HINT_SMS_OTP
+ }
+ )
+ }
+ }
+
+ private fun setupTil() {
+ loginGenericTextInputFormTextInput.textChanges()
+ .subscribe {
+ loginGenericTextInputFormTil.error = null
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun setupUi() {
+ when (params.mode) {
+ TextInputFormFragmentMode.SetEmail -> {
+ loginGenericTextInputFormTitle.text = getString(R.string.login_set_email_title)
+ loginGenericTextInputFormNotice.text = getString(R.string.login_set_email_notice)
+ loginGenericTextInputFormNotice2.setTextOrHide(null)
+ loginGenericTextInputFormTil.hint =
+ getString(if (params.mandatory) R.string.login_set_email_mandatory_hint else R.string.login_set_email_optional_hint)
+ loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ loginGenericTextInputFormOtherButton.isVisible = false
+ loginGenericTextInputFormSubmit.text = getString(R.string.login_set_email_submit)
+ }
+ TextInputFormFragmentMode.SetMsisdn -> {
+ loginGenericTextInputFormTitle.text = getString(R.string.login_set_msisdn_title)
+ loginGenericTextInputFormNotice.text = getString(R.string.login_set_msisdn_notice)
+ loginGenericTextInputFormNotice2.setTextOrHide(getString(R.string.login_set_msisdn_notice2))
+ loginGenericTextInputFormTil.hint =
+ getString(if (params.mandatory) R.string.login_set_msisdn_mandatory_hint else R.string.login_set_msisdn_optional_hint)
+ loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_PHONE
+ loginGenericTextInputFormOtherButton.isVisible = false
+ loginGenericTextInputFormSubmit.text = getString(R.string.login_set_msisdn_submit)
+ }
+ TextInputFormFragmentMode.ConfirmMsisdn -> {
+ loginGenericTextInputFormTitle.text = getString(R.string.login_msisdn_confirm_title)
+ loginGenericTextInputFormNotice.text = getString(R.string.login_msisdn_confirm_notice, params.extra)
+ loginGenericTextInputFormNotice2.setTextOrHide(null)
+ loginGenericTextInputFormTil.hint =
+ getString(R.string.login_msisdn_confirm_hint)
+ loginGenericTextInputFormTextInput.inputType = InputType.TYPE_CLASS_NUMBER
+ loginGenericTextInputFormOtherButton.isVisible = true
+ loginGenericTextInputFormOtherButton.text = getString(R.string.login_msisdn_confirm_send_again)
+ loginGenericTextInputFormSubmit.text = getString(R.string.login_msisdn_confirm_submit)
+ }
+ }
+ }
+
+ @OnClick(R.id.loginGenericTextInputFormOtherButton)
+ fun onOtherButtonClicked() {
+ when (params.mode) {
+ TextInputFormFragmentMode.ConfirmMsisdn -> {
+ loginViewModel.handle(LoginAction.SendAgainThreePid)
+ }
+ else -> {
+ // Should not happen, button is not displayed
+ }
+ }
+ }
+
+ @OnClick(R.id.loginGenericTextInputFormSubmit)
+ fun submit() {
+ cleanupUi()
+ val text = loginGenericTextInputFormTextInput.text.toString()
+
+ if (text.isEmpty()) {
+ // Perform dummy action
+ loginViewModel.handle(LoginAction.RegisterDummy)
+ } else {
+ when (params.mode) {
+ TextInputFormFragmentMode.SetEmail -> {
+ loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Email(text)))
+ }
+ TextInputFormFragmentMode.SetMsisdn -> {
+ getCountryCodeOrShowError(text)?.let { countryCode ->
+ loginViewModel.handle(LoginAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))
+ }
+ }
+ TextInputFormFragmentMode.ConfirmMsisdn -> {
+ loginViewModel.handle(LoginAction.ValidateThreePid(text))
+ }
+ }
+ }
+ }
+
+ private fun cleanupUi() {
+ loginGenericTextInputFormSubmit.hideKeyboard()
+ loginGenericTextInputFormSubmit.error = null
+ }
+
+ private fun getCountryCodeOrShowError(text: String): String? {
+ // We expect an international format for the moment (see https://github.com/vector-im/riotX-android/issues/693)
+ if (text.startsWith("+")) {
+ try {
+ val phoneNumber = PhoneNumberUtil.getInstance().parse(text, null)
+ return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode)
+ } catch (e: NumberParseException) {
+ loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_other)
+ }
+ } else {
+ loginGenericTextInputFormTil.error = getString(R.string.login_msisdn_error_not_international)
+ }
+
+ // Error
+ return null
+ }
+
+ private fun setupSubmitButton() {
+ loginGenericTextInputFormSubmit.isEnabled = false
+ loginGenericTextInputFormTextInput.textChanges()
+ .subscribe {
+ loginGenericTextInputFormSubmit.isEnabled = isInputValid(it)
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun isInputValid(input: CharSequence): Boolean {
+ return if (input.isEmpty() && !params.mandatory) {
+ true
+ } else {
+ when (params.mode) {
+ TextInputFormFragmentMode.SetEmail -> {
+ input.isEmail()
+ }
+ TextInputFormFragmentMode.SetMsisdn -> {
+ input.isNotBlank()
+ }
+ TextInputFormFragmentMode.ConfirmMsisdn -> {
+ input.isNotBlank()
+ }
+ }
+ }
+ }
+
+ override fun onError(throwable: Throwable) {
+ when (params.mode) {
+ TextInputFormFragmentMode.SetEmail -> {
+ if (throwable.is401()) {
+ // This is normal use case, we go to the mail waiting screen
+ loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))
+ } else {
+ loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
+ }
+ }
+ TextInputFormFragmentMode.SetMsisdn -> {
+ if (throwable.is401()) {
+ // This is normal use case, we go to the enter code screen
+ loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))
+ } else {
+ loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
+ }
+ }
+ TextInputFormFragmentMode.ConfirmMsisdn -> {
+ when {
+ throwable is Failure.SuccessError ->
+ // The entered code is not correct
+ loginGenericTextInputFormTil.error = getString(R.string.login_validation_code_is_not_correct)
+ throwable.is401() ->
+ // It can happen if user request again the 3pid
+ Unit
+ else ->
+ loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
+ }
+ }
+ }
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt
new file mode 100644
index 0000000000..ee39ac564b
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginMode.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+enum class LoginMode {
+ Unknown,
+ Password,
+ Sso,
+ Unsupported
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt
new file mode 100644
index 0000000000..79c6409a3f
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 im.vector.riotx.core.platform.VectorSharedAction
+
+// Supported navigation actions for LoginActivity
+sealed class LoginNavigation : VectorSharedAction {
+ object OpenServerSelection : LoginNavigation()
+ object OnServerSelectionDone : LoginNavigation()
+ object OnLoginFlowRetrieved : LoginNavigation()
+ object OnSignModeSelected : LoginNavigation()
+ object OnForgetPasswordClicked : LoginNavigation()
+ object OnResetPasswordSendThreePidDone : LoginNavigation()
+ object OnResetPasswordMailConfirmationSuccess : LoginNavigation()
+ object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation()
+
+ data class OnSendEmailSuccess(val email: String) : LoginNavigation()
+ data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation()
+
+ data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt
new file mode 100644
index 0000000000..18fcd8938b
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.extensions.isEmail
+import im.vector.riotx.core.extensions.showPassword
+import io.reactivex.Observable
+import io.reactivex.functions.BiFunction
+import io.reactivex.rxkotlin.subscribeBy
+import kotlinx.android.synthetic.main.fragment_login_reset_password.*
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is asked for email and new password to reset his password
+ */
+class LoginResetPasswordFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ private var passwordShown = false
+
+ // Show warning only once
+ private var showWarning = true
+
+ override fun getLayoutResId() = R.layout.fragment_login_reset_password
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupSubmitButton()
+ setupPasswordReveal()
+ }
+
+ private fun setupUi(state: LoginViewState) {
+ resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple)
+ }
+
+ private fun setupSubmitButton() {
+ Observable
+ .combineLatest(
+ resetPasswordEmail.textChanges().map { it.isEmail() },
+ passwordField.textChanges().map { it.isNotEmpty() },
+ BiFunction { isEmail, isPasswordNotEmpty ->
+ isEmail && isPasswordNotEmpty
+ }
+ )
+ .subscribeBy {
+ resetPasswordEmailTil.error = null
+ passwordFieldTil.error = null
+ resetPasswordSubmit.isEnabled = it
+ }
+ .disposeOnDestroyView()
+ }
+
+ @OnClick(R.id.resetPasswordSubmit)
+ fun submit() {
+ cleanupUi()
+
+ if (showWarning) {
+ showWarning = false
+ // Display a warning as Riot-Web does first
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.login_reset_password_warning_title)
+ .setMessage(R.string.login_reset_password_warning_content)
+ .setPositiveButton(R.string.login_reset_password_warning_submit) { _, _ ->
+ doSubmit()
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ } else {
+ doSubmit()
+ }
+ }
+
+ private fun doSubmit() {
+ val email = resetPasswordEmail.text.toString()
+ val password = passwordField.text.toString()
+
+ loginViewModel.handle(LoginAction.ResetPassword(email, password))
+ }
+
+ private fun cleanupUi() {
+ resetPasswordSubmit.hideKeyboard()
+ resetPasswordEmailTil.error = null
+ passwordFieldTil.error = null
+ }
+
+ private fun setupPasswordReveal() {
+ passwordShown = false
+
+ passwordReveal.setOnClickListener {
+ passwordShown = !passwordShown
+
+ renderPasswordField()
+ }
+
+ renderPasswordField()
+ }
+
+ private fun renderPasswordField() {
+ passwordField.showPassword(passwordShown)
+
+ if (passwordShown) {
+ passwordReveal.setImageResource(R.drawable.ic_eye_closed_black)
+ passwordReveal.contentDescription = getString(R.string.a11y_hide_password)
+ } else {
+ passwordReveal.setImageResource(R.drawable.ic_eye_black)
+ passwordReveal.contentDescription = getString(R.string.a11y_show_password)
+ }
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetResetPassword)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ setupUi(state)
+
+ when (state.asyncResetPassword) {
+ is Loading -> {
+ // Ensure new password is hidden
+ passwordShown = false
+ renderPasswordField()
+ }
+ is Fail -> {
+ resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
+ }
+ is Success -> {
+ loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt
new file mode 100644
index 0000000000..03053a9718
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Success
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.error.is401
+import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.*
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is asked to check his email and to click on a button once it's done
+ */
+class LoginResetPasswordMailConfirmationFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation
+
+ private fun setupUi(state: LoginViewState) {
+ resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
+ }
+
+ @OnClick(R.id.resetPasswordMailConfirmationSubmit)
+ fun submit() {
+ loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetResetPassword)
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ setupUi(state)
+
+ when (state.asyncResetMailConfirmed) {
+ is Fail -> {
+ // Link in email not yet clicked ?
+ val message = if (state.asyncResetMailConfirmed.error.is401()) {
+ getString(R.string.auth_reset_password_error_unauthorized)
+ } else {
+ errorFormatter.toHumanReadable(state.asyncResetMailConfirmed.error)
+ }
+
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+ is Success -> {
+ loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt
new file mode 100644
index 0000000000..92d75b3998
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is asked for email and new password to reset his password
+ */
+class LoginResetPasswordSuccessFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_reset_password_success
+
+ @OnClick(R.id.resetPasswordSuccessSubmit)
+ fun submit() {
+ loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetResetPassword)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt
new file mode 100644
index 0000000000..6e427d0bdb
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import com.airbnb.mvrx.withState
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.utils.openUrlInExternalBrowser
+import kotlinx.android.synthetic.main.fragment_login_server_selection.*
+import me.gujun.android.span.span
+import javax.inject.Inject
+
+/**
+ * In this screen, the user will choose between matrix.org, modular or other type of homeserver
+ */
+class LoginServerSelectionFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_server_selection
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ initTextViews()
+ }
+
+ private fun updateSelectedChoice(state: LoginViewState) {
+ state.serverType.let {
+ loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg
+ loginServerChoiceModular.isChecked = it == ServerType.Modular
+ loginServerChoiceOther.isChecked = it == ServerType.Other
+ }
+ }
+
+ private fun initTextViews() {
+ loginServerChoiceModularLearnMore.text = span {
+ text = getString(R.string.login_server_modular_learn_more)
+ textDecorationLine = "underline"
+ }
+ }
+
+ @OnClick(R.id.loginServerChoiceModularLearnMore)
+ fun learMore() {
+ openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
+ }
+
+ @OnClick(R.id.loginServerChoiceMatrixOrg)
+ fun selectMatrixOrg() {
+ if (loginServerChoiceMatrixOrg.isChecked) {
+ // Consider this is a submit
+ submit()
+ } else {
+ loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
+ }
+ }
+
+ @OnClick(R.id.loginServerChoiceModular)
+ fun selectModular() {
+ if (loginServerChoiceModular.isChecked) {
+ // Consider this is a submit
+ submit()
+ } else {
+ loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
+ }
+ }
+
+ @OnClick(R.id.loginServerChoiceOther)
+ fun selectOther() {
+ if (loginServerChoiceOther.isChecked) {
+ // Consider this is a submit
+ submit()
+ } else {
+ loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
+ }
+ }
+
+ @OnClick(R.id.loginServerSubmit)
+ fun submit() = withState(loginViewModel) { state ->
+ if (state.serverType == ServerType.MatrixOrg) {
+ // Request login flow here
+ loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url)))
+ } else {
+ loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone)
+ }
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetHomeServerType)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ updateSelectedChoice(state)
+
+ if (state.loginMode != LoginMode.Unknown) {
+ // LoginFlow for matrix.org has been retrieved
+ loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt
new file mode 100644
index 0000000000..d632ffe100
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.os.Bundle
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.core.view.isVisible
+import butterknife.OnClick
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.utils.openUrlInExternalBrowser
+import kotlinx.android.synthetic.main.fragment_login_server_url_form.*
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is prompted to enter a homeserver url
+ */
+class LoginServerUrlFormFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_server_url_form
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupHomeServerField()
+ }
+
+ private fun setupHomeServerField() {
+ loginServerUrlFormHomeServerUrl.textChanges()
+ .subscribe {
+ loginServerUrlFormHomeServerUrlTil.error = null
+ loginServerUrlFormSubmit.isEnabled = it.isNotBlank()
+ }
+ .disposeOnDestroyView()
+
+ loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ submit()
+ return@setOnEditorActionListener true
+ }
+ return@setOnEditorActionListener false
+ }
+ }
+
+ private fun setupUi(state: LoginViewState) {
+ when (state.serverType) {
+ ServerType.Modular -> {
+ loginServerUrlFormIcon.isVisible = true
+ loginServerUrlFormTitle.text = getString(R.string.login_connect_to_modular)
+ loginServerUrlFormText.text = getString(R.string.login_server_url_form_modular_text)
+ loginServerUrlFormLearnMore.isVisible = true
+ loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint)
+ loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice)
+ }
+ ServerType.Other -> {
+ loginServerUrlFormIcon.isVisible = false
+ loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
+ loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server)
+ loginServerUrlFormLearnMore.isVisible = false
+ loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint)
+ loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice)
+ }
+ else -> error("This fragment should not be displayed in matrix.org mode")
+ }
+ }
+
+ @OnClick(R.id.loginServerUrlFormLearnMore)
+ fun learnMore() {
+ openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetHomeServerUrl)
+ }
+
+ @SuppressLint("SetTextI18n")
+ @OnClick(R.id.loginServerUrlFormSubmit)
+ fun submit() {
+ cleanupUi()
+
+ // Static check of homeserver url, empty, malformed, etc.
+ var serverUrl = loginServerUrlFormHomeServerUrl.text.toString().trim()
+
+ when {
+ serverUrl.isBlank() -> {
+ loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server)
+ }
+ else -> {
+ if (serverUrl.startsWith("http").not()) {
+ serverUrl = "https://$serverUrl"
+ }
+ loginServerUrlFormHomeServerUrl.setText(serverUrl)
+ loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl))
+ }
+ }
+ }
+
+ private fun cleanupUi() {
+ loginServerUrlFormSubmit.hideKeyboard()
+ loginServerUrlFormHomeServerUrlTil.error = null
+ }
+
+ override fun onError(throwable: Throwable) {
+ loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(throwable)
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ setupUi(state)
+
+ if (state.loginMode != LoginMode.Unknown) {
+ // The home server url is valid
+ loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt
new file mode 100644
index 0000000000..625208b682
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 im.vector.riotx.core.platform.VectorSharedActionViewModel
+import javax.inject.Inject
+
+class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel()
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt
new file mode 100644
index 0000000000..0473f3d91c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt
@@ -0,0 +1,104 @@
+/*
+ * 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 androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import butterknife.OnClick
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is asked to sign up or to sign in to the homeserver
+ */
+class LoginSignUpSignInSelectionFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
+
+ private var isSsoSignIn: Boolean = false
+
+ private fun setupUi(state: LoginViewState) {
+ when (state.serverType) {
+ ServerType.MatrixOrg -> {
+ loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
+ loginSignupSigninServerIcon.isVisible = true
+ loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
+ loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
+ }
+ ServerType.Modular -> {
+ loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular)
+ loginSignupSigninServerIcon.isVisible = true
+ // TODO
+ loginSignupSigninTitle.text = getString(R.string.login_connect_to, "TODO MODULAR NAME")
+ loginSignupSigninText.text = state.homeServerUrlSimple
+ }
+ ServerType.Other -> {
+ loginSignupSigninServerIcon.isVisible = false
+ loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
+ loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
+ }
+ }
+ }
+
+ private fun setupButtons(state: LoginViewState) {
+ isSsoSignIn = state.loginMode == LoginMode.Sso
+
+ if (isSsoSignIn) {
+ loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
+ loginSignupSigninSignIn.isVisible = false
+ } else {
+ loginSignupSigninSubmit.text = getString(R.string.login_signup)
+ loginSignupSigninSignIn.isVisible = true
+ }
+ }
+
+ @OnClick(R.id.loginSignupSigninSubmit)
+ fun signUp() {
+ if (isSsoSignIn) {
+ signIn()
+ } else {
+ loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
+ }
+ }
+
+ @OnClick(R.id.loginSignupSigninSignIn)
+ fun signIn() {
+ loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn))
+ loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetSignMode)
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ setupUi(state)
+ setupButtons(state)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt
new file mode 100644
index 0000000000..ef17bea920
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import javax.inject.Inject
+
+/**
+ * In this screen, the user is viewing an introduction to what he can do with this application
+ */
+class LoginSplashFragment @Inject constructor(
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_login_splash
+
+ @OnClick(R.id.loginSplashSubmit)
+ fun getStarted() {
+ loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun resetViewModel() {
+ // Nothing to do
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt
new file mode 100644
index 0000000000..4c089174f4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 im.vector.matrix.android.api.auth.registration.FlowResult
+
+/**
+ * Transient events for Login
+ */
+sealed class LoginViewEvents {
+ data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents()
+ data class Error(val throwable: Throwable) : LoginViewEvents()
+ object OutdatedHomeserver : LoginViewEvents()
+}
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 a0a7258e2a..de76f6b416 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,31 +16,38 @@
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
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
-import im.vector.matrix.android.api.auth.Authenticator
-import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
+import im.vector.matrix.android.api.auth.AuthenticationService
+import im.vector.matrix.android.api.auth.data.LoginFlowResult
+import im.vector.matrix.android.api.auth.login.LoginWizard
+import im.vector.matrix.android.api.auth.registration.FlowResult
+import im.vector.matrix.android.api.auth.registration.RegistrationResult
+import im.vector.matrix.android.api.auth.registration.RegistrationWizard
+import im.vector.matrix.android.api.auth.registration.Stage
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.matrix.android.internal.auth.data.LoginFlowTypes
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.core.utils.DataSource
+import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import timber.log.Timber
+import java.util.concurrent.CancellationException
+/**
+ *
+ */
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
- private val authenticator: Authenticator,
+ private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder,
private val pushRuleTriggerListener: PushRuleTriggerListener,
+ private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val sessionListener: SessionListener)
: VectorViewModel(initialState) {
@@ -58,22 +65,250 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
+ val currentThreePid: String?
+ get() = registrationWizard?.currentThreePid
+
+ // True when login and password has been sent with success to the homeserver
+ val isRegistrationStarted: Boolean
+ get() = authenticationService.isRegistrationStarted
+
+ private val registrationWizard: RegistrationWizard?
+ get() = authenticationService.getRegistrationWizard()
+
+ private val loginWizard: LoginWizard?
+ get() = authenticationService.getLoginWizard()
+
private var loginConfig: LoginConfig? = null
- private val _navigationLiveData = MutableLiveData>()
- val navigationLiveData: LiveData>
- get() = _navigationLiveData
-
- private var homeServerConnectionConfig: HomeServerConnectionConfig? = null
private var currentTask: Cancelable? = null
+ private val _viewEvents = PublishDataSource()
+ val viewEvents: DataSource = _viewEvents
+
override fun handle(action: LoginAction) {
when (action) {
- is LoginAction.InitWith -> handleInitWith(action)
- is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
- is LoginAction.Login -> handleLogin(action)
- is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action)
- is LoginAction.NavigateTo -> handleNavigation(action)
+ is LoginAction.UpdateServerType -> handleUpdateServerType(action)
+ is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
+ is LoginAction.InitWith -> handleInitWith(action)
+ is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
+ is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
+ is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
+ is LoginAction.ResetPassword -> handleResetPassword(action)
+ is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
+ is LoginAction.RegisterAction -> handleRegisterAction(action)
+ is LoginAction.ResetAction -> handleResetAction(action)
+ }
+ }
+
+ private fun handleRegisterAction(action: LoginAction.RegisterAction) {
+ when (action) {
+ is LoginAction.CaptchaDone -> handleCaptchaDone(action)
+ is LoginAction.AcceptTerms -> handleAcceptTerms()
+ is LoginAction.RegisterDummy -> handleRegisterDummy()
+ is LoginAction.AddThreePid -> handleAddThreePid(action)
+ is LoginAction.SendAgainThreePid -> handleSendAgainThreePid()
+ is LoginAction.ValidateThreePid -> handleValidateThreePid(action)
+ is LoginAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action)
+ is LoginAction.StopEmailValidationCheck -> handleStopEmailValidationCheck()
+ }
+ }
+
+ private fun handleCheckIfEmailHasBeenValidated(action: LoginAction.CheckIfEmailHasBeenValidated) {
+ // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state
+ currentTask?.cancel()
+ currentTask = null
+ currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback)
+ }
+
+ private fun handleStopEmailValidationCheck() {
+ currentTask?.cancel()
+ currentTask = null
+ }
+
+ private fun handleValidateThreePid(action: LoginAction.ValidateThreePid) {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.handleValidateThreePid(action.code, registrationCallback)
+ }
+
+ private val registrationCallback = object : MatrixCallback {
+ override fun onSuccess(data: RegistrationResult) {
+ /*
+ // Simulate registration disabled
+ onFailure(Failure.ServerError(MatrixError(
+ code = MatrixError.FORBIDDEN,
+ message = "Registration is disabled"
+ ), 403))
+ */
+
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+
+ when (data) {
+ is RegistrationResult.Success -> onSessionCreated(data.session)
+ is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ if (failure !is CancellationException) {
+ _viewEvents.post(LoginViewEvents.Error(failure))
+ }
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+ }
+
+ private fun handleAddThreePid(action: LoginAction.AddThreePid) {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.addThreePid(action.threePid, object : MatrixCallback {
+ override fun onSuccess(data: RegistrationResult) {
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ _viewEvents.post(LoginViewEvents.Error(failure))
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+ })
+ }
+
+ private fun handleSendAgainThreePid() {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.sendAgainThreePid(object : MatrixCallback {
+ override fun onSuccess(data: RegistrationResult) {
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ _viewEvents.post(LoginViewEvents.Error(failure))
+ setState {
+ copy(
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+ })
+ }
+
+ private fun handleAcceptTerms() {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.acceptTerms(registrationCallback)
+ }
+
+ private fun handleRegisterDummy() {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.dummy(registrationCallback)
+ }
+
+ private fun handleRegisterWith(action: LoginAction.LoginOrRegister) {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.createAccount(
+ action.username,
+ action.password,
+ action.initialDeviceName,
+ registrationCallback
+ )
+ }
+
+ private fun handleCaptchaDone(action: LoginAction.CaptchaDone) {
+ setState { copy(asyncRegistration = Loading()) }
+ currentTask = registrationWizard?.performReCaptcha(action.captchaResponse, registrationCallback)
+ }
+
+ private fun handleResetAction(action: LoginAction.ResetAction) {
+ // Cancel any request
+ currentTask?.cancel()
+ currentTask = null
+
+ when (action) {
+ LoginAction.ResetHomeServerType -> {
+ setState {
+ copy(
+ serverType = ServerType.MatrixOrg
+ )
+ }
+ }
+ LoginAction.ResetHomeServerUrl -> {
+ authenticationService.reset()
+
+ setState {
+ copy(
+ asyncHomeServerLoginFlowRequest = Uninitialized,
+ homeServerUrl = null,
+ loginMode = LoginMode.Unknown,
+ loginModeSupportedTypes = emptyList()
+ )
+ }
+ }
+ LoginAction.ResetSignMode -> {
+ setState {
+ copy(
+ asyncHomeServerLoginFlowRequest = Uninitialized,
+ signMode = SignMode.Unknown,
+ loginMode = LoginMode.Unknown,
+ loginModeSupportedTypes = emptyList()
+ )
+ }
+ }
+ LoginAction.ResetLogin -> {
+ authenticationService.cancelPendingLoginOrRegistration()
+
+ setState {
+ copy(
+ asyncLoginAction = Uninitialized,
+ asyncRegistration = Uninitialized
+ )
+ }
+ }
+ LoginAction.ResetResetPassword -> {
+ setState {
+ copy(
+ asyncResetPassword = Uninitialized,
+ asyncResetMailConfirmed = Uninitialized,
+ resetPasswordEmail = null
+ )
+ }
+ }
+ }
+ }
+
+ private fun handleUpdateSignMode(action: LoginAction.UpdateSignMode) {
+ setState {
+ copy(
+ signMode = action.signMode
+ )
+ }
+
+ if (action.signMode == SignMode.SignUp) {
+ startRegistrationFlow()
+ } else if (action.signMode == SignMode.SignIn) {
+ startAuthenticationFlow()
+ }
+ }
+
+ private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
+ setState {
+ copy(
+ serverType = action.serverType
+ )
}
}
@@ -81,10 +316,98 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
loginConfig = action.loginConfig
}
- private fun handleLogin(action: LoginAction.Login) {
- val homeServerConnectionConfigFinal = homeServerConnectionConfig
+ private fun handleResetPassword(action: LoginAction.ResetPassword) {
+ val safeLoginWizard = loginWizard
- if (homeServerConnectionConfigFinal == null) {
+ if (safeLoginWizard == null) {
+ setState {
+ copy(
+ asyncResetPassword = Fail(Throwable("Bad configuration")),
+ asyncResetMailConfirmed = Uninitialized
+ )
+ }
+ } else {
+ setState {
+ copy(
+ asyncResetPassword = Loading(),
+ asyncResetMailConfirmed = Uninitialized
+ )
+ }
+
+ currentTask = safeLoginWizard.resetPassword(action.email, action.newPassword, object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ setState {
+ copy(
+ asyncResetPassword = Success(data),
+ resetPasswordEmail = action.email
+ )
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ // TODO Handled JobCancellationException
+ setState {
+ copy(
+ asyncResetPassword = Fail(failure)
+ )
+ }
+ }
+ })
+ }
+ }
+
+ private fun handleResetPasswordMailConfirmed() {
+ val safeLoginWizard = loginWizard
+
+ if (safeLoginWizard == null) {
+ setState {
+ copy(
+ asyncResetPassword = Uninitialized,
+ asyncResetMailConfirmed = Fail(Throwable("Bad configuration"))
+ )
+ }
+ } else {
+ setState {
+ copy(
+ asyncResetPassword = Uninitialized,
+ asyncResetMailConfirmed = Loading()
+ )
+ }
+
+ currentTask = safeLoginWizard.resetPasswordMailConfirmed(object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ setState {
+ copy(
+ asyncResetMailConfirmed = Success(data),
+ resetPasswordEmail = null
+ )
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ // TODO Handled JobCancellationException
+ setState {
+ copy(
+ asyncResetMailConfirmed = Fail(failure)
+ )
+ }
+ }
+ })
+ }
+ }
+
+ private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state ->
+ when (state.signMode) {
+ SignMode.SignIn -> handleLogin(action)
+ SignMode.SignUp -> handleRegisterWith(action)
+ else -> error("Developer error, invalid sign mode")
+ }
+ }
+
+ private fun handleLogin(action: LoginAction.LoginOrRegister) {
+ val safeLoginWizard = loginWizard
+
+ if (safeLoginWizard == null) {
setState {
copy(
asyncLoginAction = Fail(Throwable("Bad configuration"))
@@ -97,19 +420,50 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
)
}
- authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback {
- override fun onSuccess(data: Session) {
- onSessionCreated(data)
- }
+ currentTask = safeLoginWizard.login(
+ action.username,
+ action.password,
+ action.initialDeviceName,
+ object : MatrixCallback {
+ override fun onSuccess(data: Session) {
+ onSessionCreated(data)
+ }
- override fun onFailure(failure: Throwable) {
- setState {
- copy(
- asyncLoginAction = Fail(failure)
- )
- }
- }
- })
+ override fun onFailure(failure: Throwable) {
+ // TODO Handled JobCancellationException
+ setState {
+ copy(
+ asyncLoginAction = Fail(failure)
+ )
+ }
+ }
+ })
+ }
+ }
+
+ private fun startRegistrationFlow() {
+ setState {
+ copy(
+ asyncRegistration = Loading()
+ )
+ }
+
+ currentTask = registrationWizard?.getRegistrationFlow(registrationCallback)
+ }
+
+ private fun startAuthenticationFlow() {
+ // No op
+ loginWizard
+ }
+
+ private fun onFlowResponse(flowResult: FlowResult) {
+ // If dummy stage is mandatory, and password is already sent, do the dummy stage now
+ if (isRegistrationStarted
+ && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
+ handleRegisterDummy()
+ } else {
+ // Notify the user
+ _viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
}
}
@@ -123,14 +477,14 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
- private fun handleSsoLoginSuccess(action: LoginAction.SsoLoginSuccess) {
- val homeServerConnectionConfigFinal = homeServerConnectionConfig
+ private fun handleWebLoginSuccess(action: LoginAction.WebLoginSuccess) = withState { state ->
+ val homeServerConnectionConfigFinal = homeServerConnectionConfigFactory.create(state.homeServerUrl)
if (homeServerConnectionConfigFinal == null) {
// Should not happen
Timber.w("homeServerConnectionConfig is null")
} else {
- authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal, object : MatrixCallback {
+ authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials, object : MatrixCallback {
override fun onSuccess(data: Session) {
onSessionCreated(data)
}
@@ -142,59 +496,69 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
- private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) = withState { state ->
+ private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) {
+ val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
- var newConfig: HomeServerConnectionConfig? = null
- Try {
- val homeServerUri = action.homeServerUrl
- newConfig = HomeServerConnectionConfig.Builder()
- .withHomeServerUri(homeServerUri)
- .build()
- }
-
- // Do not retry if we already have flows for this config -> causes infinite focus loop
- if (newConfig?.homeServerUri?.toString() == homeServerConnectionConfig?.homeServerUri?.toString()
- && state.asyncHomeServerLoginFlowRequest is Success) return@withState
-
- currentTask?.cancel()
- homeServerConnectionConfig = newConfig
-
- val homeServerConnectionConfigFinal = homeServerConnectionConfig
-
- if (homeServerConnectionConfigFinal == null) {
+ if (homeServerConnectionConfig == null) {
// This is invalid
- setState {
- copy(
- asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format"))
- )
- }
+ _viewEvents.post(LoginViewEvents.Error(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
+ currentTask?.cancel()
+ currentTask = null
+ authenticationService.cancelPendingLoginOrRegistration()
+
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
}
- currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback {
+ currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback {
override fun onFailure(failure: Throwable) {
+ _viewEvents.post(LoginViewEvents.Error(failure))
setState {
copy(
- asyncHomeServerLoginFlowRequest = Fail(failure)
+ asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
- override fun onSuccess(data: LoginFlowResponse) {
- 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
+ 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) {
+ notSupported()
+ } else {
+ setState {
+ copy(
+ asyncHomeServerLoginFlowRequest = Uninitialized,
+ homeServerUrl = action.homeServerUrl,
+ loginMode = loginMode,
+ loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
+ )
+ }
+ }
+ }
+ is LoginFlowResult.OutdatedHomeserver -> {
+ notSupported()
+ }
}
+ }
+
+ private fun notSupported() {
+ // Notify the UI
+ _viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
- asyncHomeServerLoginFlowRequest = Success(loginMode)
+ asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
@@ -202,10 +566,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
- private fun handleNavigation(action: LoginAction.NavigateTo) {
- _navigationLiveData.postValue(LiveEvent(action.target))
- }
-
override fun onCleared() {
super.onCleared()
@@ -215,8 +575,4 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
fun getInitialHomeServerUrl(): String? {
return loginConfig?.homeServerUrl
}
-
- fun getHomeServerUrl(): String {
- return homeServerConnectionConfig?.homeServerUri?.toString() ?: ""
- }
}
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 0cc0476254..e4b3fe214a 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
@@ -16,17 +16,50 @@
package im.vector.riotx.features.login
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.MvRxState
-import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.*
data class LoginViewState(
val asyncLoginAction: Async = Uninitialized,
- val asyncHomeServerLoginFlowRequest: Async = Uninitialized
-) : MvRxState
+ val asyncHomeServerLoginFlowRequest: Async = Uninitialized,
+ val asyncResetPassword: Async = Uninitialized,
+ val asyncResetMailConfirmed: Async = Uninitialized,
+ val asyncRegistration: Async = Uninitialized,
-enum class LoginMode {
- Password,
- Sso,
- Unsupported
+ // User choices
+ @PersistState
+ val serverType: ServerType = ServerType.MatrixOrg,
+ @PersistState
+ val signMode: SignMode = SignMode.Unknown,
+ @PersistState
+ val resetPasswordEmail: String? = null,
+ @PersistState
+ val homeServerUrl: String? = null,
+
+ // Network result
+ @PersistState
+ val loginMode: LoginMode = LoginMode.Unknown,
+ @PersistState
+ // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
+ val loginModeSupportedTypes: List = emptyList()
+) : MvRxState {
+
+ fun isLoading(): Boolean {
+ return asyncLoginAction is Loading
+ || asyncHomeServerLoginFlowRequest is Loading
+ || asyncResetPassword is Loading
+ || asyncResetMailConfirmed is Loading
+ || asyncRegistration is Loading
+ }
+
+ fun isUserLogged(): Boolean {
+ return asyncLoginAction is Success
+ }
+
+ /**
+ * Ex: "https://matrix.org/" -> "matrix.org"
+ */
+ val homeServerUrlSimple: String
+ get() = (homeServerUrl ?: "")
+ .substringAfter("://")
+ .trim { it == '/' }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt
new file mode 100644
index 0000000000..2436b1d2d1
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import com.airbnb.mvrx.args
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.error.is401
+import kotlinx.android.parcel.Parcelize
+import kotlinx.android.synthetic.main.fragment_login_wait_for_email.*
+import javax.inject.Inject
+
+@Parcelize
+data class LoginWaitForEmailFragmentArgument(
+ val email: String
+) : Parcelable
+
+/**
+ * In this screen, the user is asked to check his emails
+ */
+class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() {
+
+ private val params: LoginWaitForEmailFragmentArgument by args()
+
+ override fun getLayoutResId() = R.layout.fragment_login_wait_for_email
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupUi()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ loginViewModel.handle(LoginAction.CheckIfEmailHasBeenValidated(0))
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ loginViewModel.handle(LoginAction.StopEmailValidationCheck)
+ }
+
+ private fun setupUi() {
+ loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email)
+ }
+
+ override fun onError(throwable: Throwable) {
+ if (throwable.is401()) {
+ // Try again, with a delay
+ loginViewModel.handle(LoginAction.CheckIfEmailHasBeenValidated(10_000))
+ } else {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt
similarity index 51%
rename from vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt
rename to vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt
index 38deccccaf..eac4511b57 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt
@@ -30,70 +30,66 @@ 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.api.util.JsonDict
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
-import im.vector.riotx.core.platform.OnBackPressed
-import im.vector.riotx.core.platform.VectorBaseFragment
-import kotlinx.android.synthetic.main.fragment_login_sso_fallback.*
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.utils.AssetReader
+import kotlinx.android.synthetic.main.fragment_login_web.*
import timber.log.Timber
import java.net.URLDecoder
import javax.inject.Inject
/**
- * Only login is supported for the moment
+ * This screen is displayed for SSO login and also when the application does not support login flow or registration flow
+ * of the homeserver, as a fallback to login or to create an account
*/
-class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed {
+class LoginWebFragment @Inject constructor(
+ private val assetReader: AssetReader,
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment() {
- private val viewModel: LoginViewModel by activityViewModel()
+ override fun getLayoutResId() = R.layout.fragment_login_web
- 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
+ private var isWebViewLoaded = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- setupToolbar(login_sso_fallback_toolbar)
- login_sso_fallback_toolbar.title = getString(R.string.login)
+ setupToolbar(loginWebToolbar)
+ }
- setupWebview()
+ override fun updateWithState(state: LoginViewState) {
+ setupTitle(state)
+ if (!isWebViewLoaded) {
+ setupWebView(state)
+ isWebViewLoaded = true
+ }
+ }
+
+ private fun setupTitle(state: LoginViewState) {
+ loginWebToolbar.title = when (state.signMode) {
+ SignMode.SignIn -> getString(R.string.login_signin)
+ else -> getString(R.string.login_signup)
+ }
}
@SuppressLint("SetJavaScriptEnabled")
- private fun setupWebview() {
- login_sso_fallback_webview.settings.javaScriptEnabled = true
+ private fun setupWebView(state: LoginViewState) {
+ loginWebWebView.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 += "/"
- }
+ loginWebWebView.settings.userAgentString = "Mozilla/5.0 Google"
// AppRTC requires third party cookies to work
val cookieManager = android.webkit.CookieManager.getInstance()
// clear the cookies must be cleared
if (cookieManager == null) {
- launchWebView()
+ launchWebView(state)
} else {
if (!cookieManager.hasCookies()) {
- launchWebView()
+ launchWebView(state)
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
try {
cookieManager.removeAllCookie()
@@ -101,27 +97,27 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB
Timber.e(e, " cookieManager.removeAllCookie() fails")
}
- launchWebView()
+ launchWebView(state)
} else {
try {
- cookieManager.removeAllCookies { launchWebView() }
+ cookieManager.removeAllCookies { launchWebView(state) }
} catch (e: Exception) {
Timber.e(e, " cookieManager.removeAllCookie() fails")
- launchWebView()
+ launchWebView(state)
}
}
}
}
- private fun launchWebView() {
- if (mMode == Mode.MODE_LOGIN) {
- login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/")
+ private fun launchWebView(state: LoginViewState) {
+ if (state.signMode == SignMode.SignIn) {
+ loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/")
} else {
// MODE_REGISTER
- login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/")
+ loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/")
}
- login_sso_fallback_webview.webViewClient = object : WebViewClient() {
+ loginWebWebView.webViewClient = object : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
error: SslError) {
AlertDialog.Builder(requireActivity())
@@ -136,53 +132,37 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB
}
false
})
+ .setCancelable(false)
.show()
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl)
- // on error case, close this fragment
- viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.GoBack))
+ loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl))
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
- login_sso_fallback_toolbar.subtitle = url
+ loginWebToolbar.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;" +
- " };"
-
+ val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js")
view.loadUrl(mxcJavascriptSendObjectMessage)
- if (mMode == Mode.MODE_LOGIN) {
+ if (state.signMode == SignMode.SignIn) {
// 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)
+ val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js")
+ view.loadUrl(mxcJavascriptOnLogin)
} 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 });" +
- " };"
-
+ val mxcJavascriptOnRegistered = assetReader.readAssetFile("onRegistered.js")
view.loadUrl(mxcJavascriptOnRegistered)
}
}
@@ -214,46 +194,27 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
if (null != url && url.startsWith("js:")) {
var json = url.substring(3)
- var parameters: Map? = null
+ var javascriptResponse: JavascriptResponse? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
-
- val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
-
- @Suppress("UNCHECKED_CAST")
- parameters = adapter.fromJson(json) as JsonDict?
+ val adapter = MoshiProvider.providesMoshi().adapter(JavascriptResponse::class.java)
+ javascriptResponse = adapter.fromJson(json)
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
}
// succeeds to parse parameters
- if (parameters != null) {
- val action = parameters["action"] as String
+ if (javascriptResponse != null) {
+ val action = javascriptResponse.action
- if (mMode == Mode.MODE_LOGIN) {
+ if (state.signMode == SignMode.SignIn) {
try {
if (action == "onLogin") {
- @Suppress("UNCHECKED_CAST")
- 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 safeCredentials = Credentials(
- userId = userId,
- accessToken = accessToken,
- homeServer = homeServer,
- deviceId = deviceId,
- refreshToken = null
- )
-
- viewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials))
+ val credentials = javascriptResponse.credentials
+ if (credentials != null) {
+ loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}
} catch (e: Exception) {
@@ -263,22 +224,9 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB
// 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(LoginAction.SsoLoginSuccess(credentials))
+ val credentials = javascriptResponse.credentials
+ if (credentials != null) {
+ loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}
}
@@ -291,12 +239,23 @@ class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnB
}
}
- override fun onBackPressed(): Boolean {
- return if (login_sso_fallback_webview.canGoBack()) {
- login_sso_fallback_webview.goBack()
- true
- } else {
- false
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ return when {
+ toolbarButton -> super.onBackPressed(toolbarButton)
+ loginWebWebView.canGoBack() -> loginWebWebView.goBack().run { true }
+ else -> super.onBackPressed(toolbarButton)
}
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt
new file mode 100644
index 0000000000..4c7007c137
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/ServerType.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+enum class ServerType {
+ MatrixOrg,
+ Modular,
+ Other
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt
new file mode 100644
index 0000000000..b793a0fe1d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+enum class SignMode {
+ Unknown,
+ // Account creation
+ SignUp,
+ // Login
+ SignIn
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt
new file mode 100644
index 0000000000..ce234caeb0
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/SupportedStage.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 im.vector.matrix.android.api.auth.registration.Stage
+
+/**
+ * Stage.Other is not supported, as well as any other new stages added to the SDK before it is added to the list below
+ */
+fun Stage.isSupported(): Boolean {
+ return this is Stage.ReCaptcha
+ || this is Stage.Dummy
+ || this is Stage.Msisdn
+ || this is Stage.Terms
+ || this is Stage.Email
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt
new file mode 100644
index 0000000000..52aaa9d4a4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 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.terms
+
+import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
+
+data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms,
+ var checked: Boolean = false)
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt
new file mode 100755
index 0000000000..08110f3b33
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018 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.terms
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import butterknife.OnClick
+import com.airbnb.mvrx.args
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.utils.openUrlInExternalBrowser
+import im.vector.riotx.features.login.AbstractLoginFragment
+import im.vector.riotx.features.login.LoginAction
+import im.vector.riotx.features.login.LoginViewState
+import kotlinx.android.parcel.Parcelize
+import kotlinx.android.synthetic.main.fragment_login_terms.*
+import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
+import javax.inject.Inject
+
+@Parcelize
+data class LoginTermsFragmentArgument(
+ val localizedFlowDataLoginTerms: List
+) : Parcelable
+
+/**
+ * LoginTermsFragment displays the list of policies the user has to accept
+ */
+class LoginTermsFragment @Inject constructor(
+ private val policyController: PolicyController,
+ private val errorFormatter: ErrorFormatter
+) : AbstractLoginFragment(),
+ PolicyController.PolicyControllerListener {
+
+ private val params: LoginTermsFragmentArgument by args()
+
+ override fun getLayoutResId() = R.layout.fragment_login_terms
+
+ private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList())
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ loginTermsPolicyList.setController(policyController)
+ policyController.listener = this
+
+ val list = ArrayList()
+
+ params.localizedFlowDataLoginTerms
+ .forEach {
+ list.add(LocalizedFlowDataLoginTermsChecked(it))
+ }
+
+ loginTermsViewState = LoginTermsViewState(list)
+ }
+
+ private fun renderState() {
+ policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
+
+ // Button is enabled only if all checkboxes are checked
+ loginTermsSubmit.isEnabled = loginTermsViewState.allChecked()
+ }
+
+ override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) {
+ if (isChecked) {
+ loginTermsViewState.check(localizedFlowDataLoginTerms)
+ } else {
+ loginTermsViewState.uncheck(localizedFlowDataLoginTerms)
+ }
+
+ renderState()
+ }
+
+ override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) {
+ localizedFlowDataLoginTerms.localizedUrl
+ ?.takeIf { it.isNotBlank() }
+ ?.let {
+ openUrlInExternalBrowser(requireContext(), it)
+ }
+ }
+
+ @OnClick(R.id.loginTermsSubmit)
+ internal fun submit() {
+ loginViewModel.handle(LoginAction.AcceptTerms)
+ }
+
+ override fun onError(throwable: Throwable) {
+ AlertDialog.Builder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun updateWithState(state: LoginViewState) {
+ policyController.homeServer = state.homeServerUrlSimple
+ renderState()
+ }
+
+ override fun resetViewModel() {
+ loginViewModel.handle(LoginAction.ResetLogin)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt
new file mode 100644
index 0000000000..104ea88daa
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 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.terms
+
+import com.airbnb.mvrx.MvRxState
+import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
+
+data class LoginTermsViewState(
+ val localizedFlowDataLoginTermsChecked: List
+) : MvRxState {
+ fun check(data: LocalizedFlowDataLoginTerms) {
+ localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = true
+ }
+
+ fun uncheck(data: LocalizedFlowDataLoginTerms) {
+ localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = false
+ }
+
+ fun allChecked(): Boolean {
+ return localizedFlowDataLoginTermsChecked.all { it.checked }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt
new file mode 100644
index 0000000000..c301463c2a
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 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.terms
+
+import android.view.View
+import com.airbnb.epoxy.TypedEpoxyController
+import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
+import javax.inject.Inject
+
+class PolicyController @Inject constructor() : TypedEpoxyController>() {
+
+ var listener: PolicyControllerListener? = null
+
+ var homeServer: String? = null
+
+ override fun buildModels(data: List) {
+ data.forEach { entry ->
+ policyItem {
+ id(entry.localizedFlowDataLoginTerms.policyName)
+ checked(entry.checked)
+ title(entry.localizedFlowDataLoginTerms.localizedName)
+ subtitle(homeServer)
+
+ clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) })
+ checkChangeListener { _, isChecked ->
+ listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked)
+ }
+ }
+ }
+ }
+
+ interface PolicyControllerListener {
+ fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean)
+ fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt
new file mode 100644
index 0000000000..9931d33068
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyItem.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.terms
+
+import android.view.View
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.airbnb.epoxy.EpoxyModelWithHolder
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+
+@EpoxyModelClass(layout = R.layout.item_policy)
+abstract class PolicyItem : EpoxyModelWithHolder() {
+ @EpoxyAttribute
+ var checked: Boolean = false
+
+ @EpoxyAttribute
+ var title: String? = null
+
+ @EpoxyAttribute
+ var subtitle: String? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: View.OnClickListener? = null
+
+ override fun bind(holder: Holder) {
+ holder.let {
+ it.checkbox.isChecked = checked
+ it.checkbox.setOnCheckedChangeListener(checkChangeListener)
+ it.title.text = title
+ it.subtitle.text = subtitle
+ it.view.setOnClickListener(clickListener)
+ }
+ }
+
+ // Ensure checkbox behaves as expected (remove the listener)
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ holder.checkbox.setOnCheckedChangeListener(null)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val checkbox by bind(R.id.adapter_item_policy_checkbox)
+ val title by bind(R.id.adapter_item_policy_title)
+ val subtitle by bind(R.id.adapter_item_policy_subtitle)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt
new file mode 100644
index 0000000000..1ccb7cac49
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/UrlAndName.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.terms
+
+data class UrlAndName(
+ val url: String,
+ val name: String
+)
diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt
new file mode 100644
index 0000000000..c9e6dcf3fd
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.terms
+
+import im.vector.matrix.android.api.auth.registration.TermPolicies
+import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
+
+/**
+ * This method extract the policies from the login terms parameter, regarding the user language.
+ * For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable)
+ *
+ * Example of Data:
+ *
+ * "m.login.terms": {
+ * "policies": {
+ * "privacy_policy": {
+ * "version": "1.0",
+ * "en": {
+ * "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
+ * "name": "Terms and Conditions"
+ * }
+ * }
+ * }
+ * }
+ *
+ *
+ * @param userLanguage the user language
+ * @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse
+ */
+fun TermPolicies.toLocalizedLoginTerms(userLanguage: String,
+ defaultLanguage: String = "en"): List {
+ val result = ArrayList()
+
+ val policies = get("policies")
+ if (policies is Map<*, *>) {
+ policies.keys.forEach { policyName ->
+ val localizedFlowDataLoginTerms = LocalizedFlowDataLoginTerms()
+ localizedFlowDataLoginTerms.policyName = policyName as String
+
+ val policy = policies[policyName]
+
+ // Enter this policy
+ if (policy is Map<*, *>) {
+ // Version
+ localizedFlowDataLoginTerms.version = policy["version"] as String?
+
+ var userLanguageUrlAndName: UrlAndName? = null
+ var defaultLanguageUrlAndName: UrlAndName? = null
+ var firstUrlAndName: UrlAndName? = null
+
+ // Search for language
+ policy.keys.forEach { policyKey ->
+ when (policyKey) {
+ "version" -> Unit // Ignore
+ userLanguage -> {
+ // We found the data for the user language
+ userLanguageUrlAndName = extractUrlAndName(policy[policyKey])
+ }
+ defaultLanguage -> {
+ // We found default language
+ defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey])
+ }
+ else -> {
+ if (firstUrlAndName == null) {
+ // Get at least some data
+ firstUrlAndName = extractUrlAndName(policy[policyKey])
+ }
+ }
+ }
+ }
+
+ // Copy found language data by priority
+ when {
+ userLanguageUrlAndName != null -> {
+ localizedFlowDataLoginTerms.localizedUrl = userLanguageUrlAndName!!.url
+ localizedFlowDataLoginTerms.localizedName = userLanguageUrlAndName!!.name
+ }
+ defaultLanguageUrlAndName != null -> {
+ localizedFlowDataLoginTerms.localizedUrl = defaultLanguageUrlAndName!!.url
+ localizedFlowDataLoginTerms.localizedName = defaultLanguageUrlAndName!!.name
+ }
+ firstUrlAndName != null -> {
+ localizedFlowDataLoginTerms.localizedUrl = firstUrlAndName!!.url
+ localizedFlowDataLoginTerms.localizedName = firstUrlAndName!!.name
+ }
+ }
+ }
+
+ result.add(localizedFlowDataLoginTerms)
+ }
+ }
+
+ return result
+}
+
+private fun extractUrlAndName(policyData: Any?): UrlAndName? {
+ if (policyData is Map<*, *>) {
+ val url = policyData["url"] as String?
+ val name = policyData["name"] as String?
+
+ if (url != null && name != null) {
+ return UrlAndName(url, name)
+ }
+ }
+ return null
+}
diff --git a/vector/src/main/res/anim/enter_fade_in.xml b/vector/src/main/res/anim/enter_fade_in.xml
index 292e35edde..8326050fdc 100644
--- a/vector/src/main/res/anim/enter_fade_in.xml
+++ b/vector/src/main/res/anim/enter_fade_in.xml
@@ -1,10 +1,10 @@
+ android:startOffset="@integer/default_animation_offset">
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/anim/exit_fade_out.xml b/vector/src/main/res/anim/exit_fade_out.xml
index 28934ead10..b24bb6724c 100644
--- a/vector/src/main/res/anim/exit_fade_out.xml
+++ b/vector/src/main/res/anim/exit_fade_out.xml
@@ -1,10 +1,9 @@
+ android:duration="@integer/default_animation_half">
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/color/button_background_tint_selector.xml b/vector/src/main/res/color/button_background_tint_selector.xml
new file mode 100644
index 0000000000..dd6e5f0421
--- /dev/null
+++ b/vector/src/main/res/color/button_background_tint_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/color/login_button_tint.xml b/vector/src/main/res/color/login_button_tint.xml
new file mode 100644
index 0000000000..719335766c
--- /dev/null
+++ b/vector/src/main/res/color/login_button_tint.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/bg_login_server.xml b/vector/src/main/res/drawable/bg_login_server.xml
new file mode 100644
index 0000000000..5aecd26292
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_login_server.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/bg_login_server_checked.xml b/vector/src/main/res/drawable/bg_login_server_checked.xml
new file mode 100644
index 0000000000..1aea622462
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_login_server_checked.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/bg_login_server_selector.xml b/vector/src/main/res/drawable/bg_login_server_selector.xml
new file mode 100644
index 0000000000..57be1e5d54
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_login_server_selector.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/ic_login_splash_lock.xml b/vector/src/main/res/drawable/ic_login_splash_lock.xml
new file mode 100644
index 0000000000..26470cefce
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_login_splash_lock.xml
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_login_splash_message_circle.xml b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml
new file mode 100644
index 0000000000..81b5e9476a
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_login_splash_message_circle.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_login_splash_sliders.xml b/vector/src/main/res/drawable/ic_login_splash_sliders.xml
new file mode 100644
index 0000000000..b7c850eea7
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_login_splash_sliders.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_logo_matrix_org.xml b/vector/src/main/res/drawable/ic_logo_matrix_org.xml
new file mode 100644
index 0000000000..13a05fba4f
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_logo_matrix_org.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_logo_modular.xml b/vector/src/main/res/drawable/ic_logo_modular.xml
new file mode 100644
index 0000000000..c95ee66b86
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_logo_modular.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000000..0add6040a7
--- /dev/null
+++ b/vector/src/main/res/layout/activity_login.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml
index 66a040b935..f8450d1e6e 100644
--- a/vector/src/main/res/layout/fragment_create_direct_room.xml
+++ b/vector/src/main/res/layout/fragment_create_direct_room.xml
@@ -107,7 +107,7 @@
diff --git a/vector/src/main/res/layout/fragment_loading.xml b/vector/src/main/res/layout/fragment_loading.xml
index 96bafda319..ae605097cd 100644
--- a/vector/src/main/res/layout/fragment_loading.xml
+++ b/vector/src/main/res/layout/fragment_loading.xml
@@ -4,12 +4,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+
+ android:layout_height="match_parent"
+ android:background="?riotx_background">
-
+
+
+
+
+
+ style="@style/LoginFormContainer"
+ android:orientation="vertical">
+ tools:src="@drawable/ic_logo_matrix_org" />
+ android:layout_marginTop="@dimen/layout_vertical_margin"
+ android:textAppearance="@style/TextAppearance.Vector.Login.Title"
+ tools:text="@string/login_signin_to" />
+
+
+ android:layout_marginTop="32dp"
+ android:hint="@string/login_signup_username_hint"
+ app:errorEnabled="true">
+ android:hint="@string/login_signup_password_hint"
+ app:errorEnabled="true"
+ app:errorIconDrawable="@null">
-
+ android:layout_marginTop="22dp"
+ android:orientation="horizontal">
-
+ android:layout_gravity="start"
+ android:text="@string/auth_forgot_password" />
-
+
-
-
-
+
-
-
-
-
-
+
diff --git a/vector/src/main/res/layout/fragment_login_captcha.xml b/vector/src/main/res/layout/fragment_login_captcha.xml
new file mode 100644
index 0000000000..2f8a4f9b0d
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_captcha.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml
new file mode 100644
index 0000000000..5421d5eac8
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_generic_text_input_form.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_reset_password.xml b/vector/src/main/res/layout/fragment_login_reset_password.xml
new file mode 100644
index 0000000000..506afbe519
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_reset_password.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation.xml b/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation.xml
new file mode 100644
index 0000000000..ec2ae5cda3
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_reset_password_mail_confirmation.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_reset_password_success.xml b/vector/src/main/res/layout/fragment_login_reset_password_success.xml
new file mode 100644
index 0000000000..fc5aea3394
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_reset_password_success.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml
new file mode 100644
index 0000000000..c97b32bd21
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_server_selection.xml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml
new file mode 100644
index 0000000000..c8c2bb9a57
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml
new file mode 100644
index 0000000000..3de579c6d9
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml
new file mode 100644
index 0000000000..44a81df539
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_splash.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_terms.xml b/vector/src/main/res/layout/fragment_login_terms.xml
new file mode 100644
index 0000000000..e7daebfce7
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_terms.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_wait_for_email.xml b/vector/src/main/res/layout/fragment_login_wait_for_email.xml
new file mode 100644
index 0000000000..511e19ca43
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_login_wait_for_email.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_web.xml
similarity index 84%
rename from vector/src/main/res/layout/fragment_login_sso_fallback.xml
rename to vector/src/main/res/layout/fragment_login_web.xml
index e83680d2cd..cd673d03bf 100644
--- a/vector/src/main/res/layout/fragment_login_sso_fallback.xml
+++ b/vector/src/main/res/layout/fragment_login_web.xml
@@ -3,10 +3,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="?riotx_background"
android:orientation="vertical">
diff --git a/vector/src/main/res/layout/fragment_public_rooms.xml b/vector/src/main/res/layout/fragment_public_rooms.xml
index ceb45b275b..acc9bb5673 100644
--- a/vector/src/main/res/layout/fragment_public_rooms.xml
+++ b/vector/src/main/res/layout/fragment_public_rooms.xml
@@ -45,7 +45,7 @@
@@ -68,7 +68,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_room_filter_footer.xml b/vector/src/main/res/layout/item_room_filter_footer.xml
index 75a77d074d..00cede6f1f 100644
--- a/vector/src/main/res/layout/item_room_filter_footer.xml
+++ b/vector/src/main/res/layout/item_room_filter_footer.xml
@@ -16,7 +16,7 @@
+
+
+
+
+
diff --git a/vector/src/main/res/values-v21/styles_login.xml b/vector/src/main/res/values-v21/styles_login.xml
new file mode 100644
index 0000000000..22eeec5528
--- /dev/null
+++ b/vector/src/main/res/values-v21/styles_login.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values-v21/theme_black.xml b/vector/src/main/res/values-v21/theme_black.xml
index 74ec2cd9e2..6c6d78879e 100644
--- a/vector/src/main/res/values-v21/theme_black.xml
+++ b/vector/src/main/res/values-v21/theme_black.xml
@@ -11,7 +11,6 @@
- @transition/image_preview_transition
- @transition/image_preview_transition
-
diff --git a/vector/src/main/res/values-v21/theme_status.xml b/vector/src/main/res/values-v21/theme_status.xml
index 3deb65a9d6..90c4e39b6f 100644
--- a/vector/src/main/res/values-v21/theme_status.xml
+++ b/vector/src/main/res/values-v21/theme_status.xml
@@ -7,6 +7,10 @@
- true
+
+
+ - @transition/image_preview_transition
+ - @transition/image_preview_transition
diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml
index 365a6bee34..cc08a3a85d 100644
--- a/vector/src/main/res/values/colors_riot.xml
+++ b/vector/src/main/res/values/colors_riot.xml
@@ -122,7 +122,7 @@
#FFFFFFFF
- #FF7F7F7F
+ #FFFFFFFF
#368BD6
diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
index aec2a4c535..3763bc33f6 100644
--- a/vector/src/main/res/values/colors_riotx.xml
+++ b/vector/src/main/res/values/colors_riotx.xml
@@ -5,6 +5,7 @@
#FF03B381
+ #3F03B381
#FFFF4B55
#FF61708B
diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml
index 4d81a54e0a..9d653b67dd 100755
--- a/vector/src/main/res/values/config.xml
+++ b/vector/src/main/res/values/config.xml
@@ -5,7 +5,6 @@
https://matrix.org
- https://matrix.org
https://piwik.riot.im
https://riot.im/bugreports/submit
diff --git a/vector/src/main/res/values/integers.xml b/vector/src/main/res/values/integers.xml
index 59c1327f30..75e8bb6f9a 100644
--- a/vector/src/main/res/values/integers.xml
+++ b/vector/src/main/res/values/integers.xml
@@ -1,7 +1,11 @@
- 500
+ 200
+
+ 400
+
+ 200
0
diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
index 8d8be693e1..5299d0db1f 100644
--- a/vector/src/main/res/values/strings_riotX.xml
+++ b/vector/src/main/res/values/strings_riotX.xml
@@ -23,5 +23,118 @@
%1$s made the room public to whoever knows the link.
%1$s made the room invite only.
+ Liberate your communication
+ Chat with people directly or in groups
+ Keep conversations private with encryption
+ Extend & customise your experience
+ Get started
+
+ Select a server
+ Just like email, accounts have one home, although you can talk to anyone
+ Join millions free on the largest public server
+ Premium hosting for organisations
+ Learn more
+ Other
+ Custom & advanced settings
+
+ Continue
+
+ Connect to %1$s
+ Connect to Modular
+ Connect to a custom server
+
+ Sign in to %1$s
+ Sign Up
+ Sign In
+ Continue with SSO
+
+ Modular Address
+ Address
+ Premium hosting for organisations
+ Enter the address of the Modular Riot or Server you want to use
+ Enter the address of a server or a Riot you want to connect to
+
+ An error occurred when loading the page: %1$s (%2$d)
+ The application is not able to signin to this homeserver. The homeserver supports the following signin type(s): %1$s.\n\nDo you want to signin using a web client?
+ Sorry, this server isn’t accepting new accounts.
+ The application is not able to create an account on this homeserver.\n\nDo you want to signup using a web client?
+
+ This email is not associated to any account.
+
+
+ Reset password on %1$s
+ A verification email will be sent to your inbox to confirm setting your new password.
+ Next
+ Email
+ New password
+
+ Warning!
+ Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.
+ Continue
+
+ This email is not linked to any account
+
+ Check your inbox
+
+ A verification email was sent to %1$s.
+ Tap on the link to confirm your new password. Once you\'ve followed the link it contains, click below.
+ I have verified my email address
+
+ Success!
+ Your password has been reset.
+ You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.
+ Back to Sign In
+
+ Warning
+ Your password is not yet changed.\n\nStop the password change process?
+
+ Set email address
+ Set an email to recover your account. Later, you can optionally allow people you know to discover you by your email.
+ Email
+ Email (optional)
+ Next
+
+ Set phone number
+ Set a phone number to optionally allow people you know to discover you.
+ Please use the international format.
+ Phone number
+ Phone number (optional)
+ Next
+
+ Confirm phone number
+
+ We just sent a code to %1$s. Enter it below to verify it’s you.
+ Enter code
+ Send again
+ Next
+
+ "International phone numbers must start with '+'"
+ "Phone number seems invalid. Please check it"
+
+
+ Sign up to %1$s
+ Username
+ Password
+ Next
+ That username is taken
+ Warning
+ Your account is not created yet.\n\nStop the registration process?
+
+ Select matrix.org
+ Select modular
+ Select a custom homeserver
+ Please perform the captcha challenge
+ Accept terms to continue
+
+ Please check your email
+ We just sent an email to %1$s.\nPlease click on the link it contains to continue the account creation.
+ The entered code is not correct. Please check.
+ Outdated homeserver
+ This homeserver is running too old a version to connect to. Ask your homeserver admin to upgrade.
+
+
+ - Too many requests have been sent. You can retry in %1$d second…
+ - Too many requests have been sent. You can retry in %1$d seconds…
+
diff --git a/vector/src/main/res/values/styles_login.xml b/vector/src/main/res/values/styles_login.xml
new file mode 100644
index 0000000000..3bcda048dc
--- /dev/null
+++ b/vector/src/main/res/values/styles_login.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
index c5b04de730..798c7ced87 100644
--- a/vector/src/main/res/values/styles_riot.xml
+++ b/vector/src/main/res/values/styles_riot.xml
@@ -122,7 +122,7 @@
using colorControlHighlight as an overlay for focused and pressed states.
-->
+
+
+
+
+
+
\ No newline at end of file