? = 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/session/file/FileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt
index e1694199ed..4d9cff3e92 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt
@@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.file
import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import java.io.File
@@ -47,5 +48,5 @@ interface FileService {
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
- callback: MatrixCallback)
+ callback: MatrixCallback): Cancelable
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
index 5af5183dfa..385699b4db 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
@@ -72,7 +72,7 @@ interface RelationService {
*/
fun editTextMessage(targetEventId: String,
msgType: String,
- newBodyText: String,
+ newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable
@@ -97,12 +97,14 @@ interface RelationService {
/**
* Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
+ * The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
+ * by the sdk into pills.
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/
fun replyToMessage(eventReplied: TimelineEvent,
- replyText: String,
+ replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData>
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
index 8c783837a2..bdae5eaaa6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
@@ -29,20 +29,23 @@ interface SendService {
/**
* Method to send a text message asynchronously.
+ * The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
+ * by the sdk into pills.
* @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @return a [Cancelable]
*/
- fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
+ fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
/**
* Method to send a text message with a formatted body.
* @param text the text message to send
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
+ * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @return a [Cancelable]
*/
- fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable
+ fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
/**
* Method to send a media asynchronously.
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt
new file mode 100644
index 0000000000..4cd8080dc3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.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.api.session.room.send
+
+/**
+ * Tag class for spans that should mention a user.
+ * These Spans will be transformed into pills when detected in message to send
+ */
+interface UserMentionSpan {
+ val displayName: String
+ val userId: String
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
index c03effd7ad..85dbdcaa19 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
@@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline
*/
interface Timeline {
- var listener: Listener?
+ val timelineID: String
val isLive: Boolean
+ fun addListener(listener: Listener): Boolean
+
+ fun removeListener(listener: Listener): Boolean
+
+ fun removeAllListeners()
+
/**
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
*/
@@ -98,7 +104,7 @@ interface Timeline {
interface Listener {
/**
* Call when the timeline has been updated through pagination or sync.
- * @param snapshot the most uptodate snapshot
+ * @param snapshot the most up to date snapshot
*/
fun onUpdated(snapshot: List)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
index ad747efee9..ed7f49aa46 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
@@ -41,8 +41,7 @@ data class TimelineEvent(
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null,
- val readReceipts: List = emptyList(),
- val hasReadMarker: Boolean = false
+ val readReceipts: List = emptyList()
) {
val metadata = HashMap()
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..1b15995ae6 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
@@ -22,6 +22,8 @@ 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 io.realm.exceptions.RealmPrimaryKeyConstraintException
+import timber.log.Timber
import javax.inject.Inject
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
@@ -30,43 +32,45 @@ 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) {
awaitTransaction(realmConfiguration) {
val entity = mapper.map(sessionParams)
if (entity != null) {
- it.insert(entity)
+ try {
+ it.insert(entity)
+ } catch (e: RealmPrimaryKeyConstraintException) {
+ Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials")
+ it.insertOrUpdate(entity)
+ }
}
}
}
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/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
index 5a7e28b70f..a12f6e40ce 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt
@@ -37,6 +37,8 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
import io.realm.RealmConfiguration
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
import retrofit2.Retrofit
import java.io.File
@@ -66,6 +68,13 @@ internal abstract class CryptoModule {
.build()
}
+ @JvmStatic
+ @Provides
+ @SessionScope
+ fun providesCryptoCoroutineScope(): CoroutineScope {
+ return CoroutineScope(SupervisorJob())
+ }
+
@JvmStatic
@Provides
@CryptoDatabase
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
index cf5506a443..c50b9e2e10 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
@@ -132,7 +132,8 @@ internal class DefaultCryptoService @Inject constructor(
private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
- private val taskExecutor: TaskExecutor
+ private val taskExecutor: TaskExecutor,
+ private val cryptoCoroutineScope: CoroutineScope
) : CryptoService {
private val uiHandler = Handler(Looper.getMainLooper())
@@ -243,7 +244,8 @@ internal class DefaultCryptoService @Inject constructor(
return
}
isStarting.set(true)
- GlobalScope.launch(coroutineDispatchers.crypto) {
+
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync)
}
}
@@ -269,10 +271,9 @@ internal class DefaultCryptoService @Inject constructor(
isStarted.set(true)
},
{
- Timber.e("Start failed: $it")
- delay(1000)
isStarting.set(false)
- internalStart(isInitialSync)
+ isStarted.set(false)
+ Timber.e(it, "Start failed")
}
)
}
@@ -281,9 +282,12 @@ internal class DefaultCryptoService @Inject constructor(
* Close the crypto
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
+
+ outgoingRoomKeyRequestManager.stop()
+
olmDevice.release()
cryptoStore.close()
- outgoingRoomKeyRequestManager.stop()
}
// Aways enabled on RiotX
@@ -305,19 +309,21 @@ internal class DefaultCryptoService @Inject constructor(
* @param syncResponse the syncResponse
*/
fun onSyncCompleted(syncResponse: SyncResponse) {
- GlobalScope.launch(coroutineDispatchers.crypto) {
- if (syncResponse.deviceLists != null) {
- deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
- }
- if (syncResponse.deviceOneTimeKeysCount != null) {
- val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
- oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
- }
- if (isStarted()) {
- // Make sure we process to-device messages before generating new one-time-keys #2782
- deviceListManager.refreshOutdatedDeviceLists()
- oneTimeKeysUploader.maybeUploadOneTimeKeys()
- incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ runCatching {
+ if (syncResponse.deviceLists != null) {
+ deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
+ }
+ if (syncResponse.deviceOneTimeKeysCount != null) {
+ val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
+ oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
+ }
+ if (isStarted()) {
+ // Make sure we process to-device messages before generating new one-time-keys #2782
+ deviceListManager.refreshOutdatedDeviceLists()
+ oneTimeKeysUploader.maybeUploadOneTimeKeys()
+ incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
+ }
}
}
}
@@ -511,7 +517,7 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback) {
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false)
@@ -571,7 +577,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) {
- GlobalScope.launch {
+ cryptoCoroutineScope.launch {
val result = runCatching {
withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
@@ -621,7 +627,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the event
*/
fun onToDeviceEvent(event: Event) {
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event)
@@ -661,7 +667,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the encryption event.
*/
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId)
try {
loadRoomMembersTask.execute(params)
@@ -753,7 +759,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the exported keys
*/
override fun exportRoomKeys(password: String, callback: MatrixCallback) {
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.foldToCallback(callback)
@@ -791,7 +797,7 @@ internal class DefaultCryptoService @Inject constructor(
password: String,
progressListener: ProgressListener?,
callback: MatrixCallback) {
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
Timber.v("## importRoomKeys starts")
@@ -839,7 +845,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun checkUnknownDevices(userIds: List, callback: MatrixCallback) {
// force the refresh to ensure that the devices list is up-to-date
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
val keys = deviceListManager.downloadKeys(userIds, true)
val unknownDevices = getUnknownDevices(keys)
@@ -999,7 +1005,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) {
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
deviceListManager.downloadKeys(userIds, forceDownload)
}.foldToCallback(callback)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
index 3c8d70f2f1..e8d8bf0f35 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
@@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
-import kotlin.collections.ArrayList
@SessionScope
internal class IncomingRoomKeyRequestManager @Inject constructor(
@@ -51,7 +50,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
*
* @param event the announcement event.
*/
- suspend fun onRoomKeyRequestEvent(event: Event) {
+ fun onRoomKeyRequestEvent(event: Event) {
val roomKeyShare = event.getClearContent().toModel()
when (roomKeyShare?.action) {
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
@@ -78,7 +77,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in
- Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
+ Timber.w("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
return
}
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@@ -86,11 +85,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) {
- Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
+ Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
continue
}
if (!decryptor.hasKeysForKeyRequest(request)) {
- Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
+ Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.deleteIncomingRoomKeyRequest(request)
continue
}
@@ -139,7 +138,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
if (null != receivedRoomKeyRequestCancellations) {
for (request in receivedRoomKeyRequestCancellations!!) {
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
- + ":" + request.deviceId + " id " + request.requestId)
+ + ":" + request.deviceId + " id " + request.requestId)
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
index 68aaaf3831..6171b32811 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
@@ -764,7 +764,7 @@ internal class MXOlmDevice @Inject constructor(
return session
}
} else {
- Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
+ Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt
index e6b57d149f..a0483335e5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt
@@ -42,8 +42,6 @@ internal class OneTimeKeysUploader @Inject constructor(
private var lastOneTimeKeyCheck: Long = 0
private var oneTimeKeyCount: Int? = null
- private var lastPublishedOneTimeKeys: Map>? = null
-
/**
* Stores the current one_time_key count which will be handled later (in a call of
* _onSyncCompleted). The count is e.g. coming from a /sync response.
@@ -59,10 +57,12 @@ internal class OneTimeKeysUploader @Inject constructor(
*/
suspend fun maybeUploadOneTimeKeys() {
if (oneTimeKeyCheckInProgress) {
+ Timber.v("maybeUploadOneTimeKeys: already in progress")
return
}
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
// we've done a key upload recently.
+ Timber.v("maybeUploadOneTimeKeys: executed too recently")
return
}
@@ -79,12 +79,8 @@ internal class OneTimeKeysUploader @Inject constructor(
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
- if (oneTimeKeyCount != null) {
- uploadOTK(oneTimeKeyCount!!, keyLimit)
- } else {
- // ask the server how many keys we have
- val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
- val response = uploadKeysTask.execute(uploadKeysParams)
+ val oneTimeKeyCountFromSync = oneTimeKeyCount
+ if (oneTimeKeyCountFromSync != null) {
// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
@@ -96,14 +92,17 @@ internal class OneTimeKeysUploader @Inject constructor(
// private keys clogging up our local storage.
// So we need some kind of engineering compromise to balance all of
// these factors.
- // TODO Why we do not set oneTimeKeyCount here?
- // TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
- val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
- uploadOTK(keyCount, keyLimit)
+ try {
+ val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
+ Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
+ } finally {
+ oneTimeKeyCheckInProgress = false
+ }
+ } else {
+ Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
+ oneTimeKeyCheckInProgress = false
+ lastOneTimeKeyCheck = 0
}
- Timber.v("## uploadKeys() : success")
- oneTimeKeyCount = null
- oneTimeKeyCheckInProgress = false
}
/**
@@ -111,53 +110,51 @@ internal class OneTimeKeysUploader @Inject constructor(
*
* @param keyCount the key count
* @param keyLimit the limit
+ * @return the number of uploaded keys
*/
- private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) {
+ private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
if (keyLimit <= keyCount) {
// If we don't need to generate any more keys then we are done.
- return
+ return 0
}
val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
olmDevice.generateOneTimeKeys(keysThisLoop)
- val response = uploadOneTimeKeys()
+ val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
+ olmDevice.markKeysAsPublished()
+
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
- uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
+ // Maybe upload other keys
+ return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
} else {
- Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
+ Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
}
}
/**
- * Upload my user's one time keys.
+ * Upload curve25519 one time keys.
*/
- private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
- val oneTimeKeys = olmDevice.getOneTimeKeys()
+ private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse {
val oneTimeJson = mutableMapOf()
- val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
+ val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap()
- if (null != curve25519Map) {
- for ((key_id, value) in curve25519Map) {
- val k = mutableMapOf()
- k["key"] = value
+ curve25519Map.forEach { (key_id, value) ->
+ val k = mutableMapOf()
+ k["key"] = value
- // the key is also signed
- val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
+ // the key is also signed
+ val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
- k["signatures"] = objectSigner.signObject(canonicalJson)
+ k["signatures"] = objectSigner.signObject(canonicalJson)
- oneTimeJson["signed_curve25519:$key_id"] = k
- }
+ oneTimeJson["signed_curve25519:$key_id"] = k
}
// For now, we set the device id explicitly, as we may not be using the
// same one as used in login.
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
- val response = uploadKeysTask.execute(uploadParams)
- lastPublishedOneTimeKeys = oneTimeKeys
- olmDevice.markKeysAsPublished()
- return response
+ return uploadKeysTask.execute(uploadParams)
}
companion object {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt
index 86e8a1825c..5320b84b0e 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt
@@ -63,6 +63,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
*/
fun stop() {
isClientRunning = false
+ stopTimer()
}
/**
@@ -171,6 +172,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
}
+ private fun stopTimer() {
+ BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
+ }
+
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
@@ -187,7 +192,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
if (null == outgoingRoomKeyRequest) {
- Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
+ Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 0230141e1b..81ac1403df 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -34,7 +34,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -46,8 +46,9 @@ internal class MXMegolmDecryption(private val userId: String,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
- private val coroutineDispatchers: MatrixCoroutineDispatchers)
- : IMXDecrypting {
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope
+) : IMXDecrypting {
var newSessionListener: NewSessionListener? = null
@@ -61,7 +62,7 @@ internal class MXMegolmDecryption(private val userId: String,
return decryptEvent(event, timeline, true)
}
- private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
+ private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
@@ -292,7 +293,7 @@ internal class MXMegolmDecryption(private val userId: String,
return
}
val userId = request.userId ?: return
- GlobalScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching {
val deviceId = request.deviceId
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
index b7329221ab..7cddd27779 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
@@ -25,17 +25,21 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import kotlinx.coroutines.CoroutineScope
import javax.inject.Inject
-internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String,
- private val olmDevice: MXOlmDevice,
- private val deviceListManager: DeviceListManager,
- private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
- private val messageEncrypter: MessageEncrypter,
- private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
- private val cryptoStore: IMXCryptoStore,
- private val sendToDeviceTask: SendToDeviceTask,
- private val coroutineDispatchers: MatrixCoroutineDispatchers) {
+internal class MXMegolmDecryptionFactory @Inject constructor(
+ @UserId private val userId: String,
+ private val olmDevice: MXOlmDevice,
+ private val deviceListManager: DeviceListManager,
+ private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
+ private val messageEncrypter: MessageEncrypter,
+ private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
+ private val cryptoStore: IMXCryptoStore,
+ private val sendToDeviceTask: SendToDeviceTask,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope
+) {
fun create(): MXMegolmDecryption {
return MXMegolmDecryption(
@@ -47,6 +51,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val
ensureOlmSessionsForDevicesAction,
cryptoStore,
sendToDeviceTask,
- coroutineDispatchers)
+ coroutineDispatchers,
+ cryptoCoroutineScope)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
index b3ee138591..1cc1a8a05a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
@@ -59,7 +59,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.awaitCallback
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmException
@@ -102,7 +102,8 @@ internal class KeysBackup @Inject constructor(
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor
private val taskExecutor: TaskExecutor,
- private val coroutineDispatchers: MatrixCoroutineDispatchers
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope
) : KeysBackupService {
private val uiHandler = Handler(Looper.getMainLooper())
@@ -143,7 +144,7 @@ internal class KeysBackup @Inject constructor(
override fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?,
callback: MatrixCallback) {
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption()
@@ -233,7 +234,7 @@ internal class KeysBackup @Inject constructor(
}
override fun deleteBackup(version: String, callback: MatrixCallback?) {
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
@@ -344,9 +345,7 @@ internal class KeysBackup @Inject constructor(
}
})
}
- }
-
- keysBackupStateManager.addListener(keysBackupStateListener!!)
+ }.also { keysBackupStateManager.addListener(it) }
backupKeys()
}
@@ -448,7 +447,7 @@ internal class KeysBackup @Inject constructor(
callback.onFailure(IllegalArgumentException("Missing element"))
} else {
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
@@ -523,7 +522,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val isValid = withContext(coroutineDispatchers.crypto) {
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
}
@@ -543,7 +542,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback) {
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion, null)
}
@@ -614,7 +613,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback) {
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val decryption = withContext(coroutineDispatchers.crypto) {
// Check if the recovery is valid before going any further
@@ -695,7 +694,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback) {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
@@ -729,8 +728,8 @@ internal class KeysBackup @Inject constructor(
* parameters and always returns a KeysBackupData object through the Callback
*/
private suspend fun getKeys(sessionId: String?,
- roomId: String?,
- version: String): KeysBackupData {
+ roomId: String?,
+ version: String): KeysBackupData {
return if (roomId != null && sessionId != null) {
// Get key for the room and for the session
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
@@ -1154,7 +1153,7 @@ internal class KeysBackup @Inject constructor(
keysBackupStateManager.state = KeysBackupState.BackingUp
- GlobalScope.launch(coroutineDispatchers.main) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Timber.v("backupKeys: 2 - Encrypting keys")
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
index fd7a5e8e7a..db05f473b1 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt
@@ -53,10 +53,10 @@ internal class DefaultUploadKeysTask @Inject constructor(private val cryptoApi:
}
return executeRequest {
- if (encodedDeviceId.isNullOrBlank()) {
- apiCall = cryptoApi.uploadKeys(body)
+ apiCall = if (encodedDeviceId.isBlank()) {
+ cryptoApi.uploadKeys(body)
} else {
- apiCall = cryptoApi.uploadKeys(encodedDeviceId, body)
+ cryptoApi.uploadKeys(encodedDeviceId, body)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
index 881d7ce2c5..bc806a56a4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt
@@ -83,7 +83,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
try {
File(directory, file).deleteRecursively()
} catch (e: Exception) {
- Timber.e(e, "Unable to move files")
+ Timber.e(e, "Unable to delete files")
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
index e9ffa140c9..826b35254e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
@@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
-import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String,
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
- ?: ReadReceiptsSummaryEntity(eventId, roomId)
+ ?: ReadReceiptsSummaryEntity(eventId, roomId)
// Update RR for the sender of a new message with a dummy one
@@ -168,7 +167,6 @@ internal fun ChunkEntity.add(roomId: String,
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity
- it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
@@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String,
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
- PaginationDirection.FORWARDS -> forwardsDisplayIndex
- PaginationDirection.BACKWARDS -> backwardsDisplayIndex
- } ?: defaultValue
+ PaginationDirection.FORWARDS -> forwardsDisplayIndex
+ PaginationDirection.BACKWARDS -> backwardsDisplayIndex
+ } ?: defaultValue
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
- PaginationDirection.FORWARDS -> forwardsStateIndex
- PaginationDirection.BACKWARDS -> backwardsStateIndex
- } ?: defaultValue
+ PaginationDirection.FORWARDS -> forwardsStateIndex
+ PaginationDirection.BACKWARDS -> backwardsStateIndex
+ } ?: defaultValue
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
index 8046ecbff0..9959f940b6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
@@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
}
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
- ?: Event("", timelineEventEntity.eventId),
+ ?: Event("", timelineEventEntity.eventId),
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
@@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending {
it.originServerTs
- } ?: emptyList(),
- hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true
+ } ?: emptyList()
)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt
index 9e78c94f88..4d16d120d8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt
@@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
-import io.realm.RealmResults
-import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadMarkerEntity(
@@ -27,8 +25,5 @@ internal open class ReadMarkerEntity(
var eventId: String = ""
) : RealmObject() {
- @LinkingObjects("readMarker")
- val timelineEvent: RealmResults? = null
-
companion object
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
index fd3a427781..235910b1ea 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt
@@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null,
- var readReceipts: ReadReceiptsSummaryEntity? = null,
- var readMarker: ReadMarkerEntity? = null
+ var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() {
@LinkingObjects("timelineEvents")
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt
index 4f64f2896f..6902d39a82 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt
@@ -16,26 +16,27 @@
package im.vector.matrix.android.internal.database.query
-import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.model.FilterEntity
import im.vector.matrix.android.internal.session.filter.FilterFactory
import io.realm.Realm
+import io.realm.kotlin.createObject
import io.realm.kotlin.where
+/**
+ * Get the current filter
+ */
+internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? {
+ return realm.where().findFirst()
+}
+
/**
* Get the current filter, create one if it does not exist
*/
-internal suspend fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity {
- var filter = realm.where().findFirst()
- if (filter == null) {
- filter = FilterEntity().apply {
- filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
- roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
- filterId = ""
- }
- awaitTransaction(realm.configuration) {
- it.insert(filter)
- }
- }
- return filter
+internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
+ return get(realm) ?: realm.createObject()
+ .apply {
+ filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
+ roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
+ filterId = ""
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt
index 061634a9da..d95dc58574 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt
@@ -22,13 +22,9 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
-internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery {
- val query = realm.where()
+internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery {
+ return realm.where()
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
- if (eventId != null) {
- query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
- }
- return query
}
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
index 0a925ac1ab..c214886ec8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
@@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.database.model.ChunkEntity
+import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
+import io.realm.Realm
internal fun isEventRead(monarchy: Monarchy,
userId: String?,
@@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy,
isEventRead = if (eventToCheck?.sender == userId) {
true
} else {
- val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm
- val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE
+ val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
+ ?: return@doWithRealm
+ val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
+ ?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex <= readReceiptIndex
@@ -49,3 +53,21 @@ internal fun isEventRead(monarchy: Monarchy,
return isEventRead
}
+
+internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
+ roomId: String?,
+ eventId: String?): Boolean {
+ if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
+ return false
+ }
+ return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
+ val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false
+ val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
+
+ val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
+ val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex
+ ?: Int.MIN_VALUE
+ val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
+ eventToCheckIndex <= readMarkerIndex
+ }
+}
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/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
index e530bafb18..868d63665a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt
@@ -22,12 +22,14 @@ import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
+import im.vector.matrix.android.internal.util.toCancelable
import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -55,8 +57,8 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
- callback: MatrixCallback) {
- GlobalScope.launch(coroutineDispatchers.main) {
+ callback: MatrixCallback): Cancelable {
+ return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = getFolder(downloadMode, id)
@@ -96,7 +98,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
}
}
.foldToCallback(callback)
- }
+ }.toCancelable()
}
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt
index c8bd5154a2..3f653571b7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultInitialSyncProgressService.kt
@@ -101,7 +101,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
val parentProgress = (currentProgress * parentWeight).toInt()
it.setProgress(offset + parentProgress)
} ?: run {
- Timber.e("--- ${leaf().nameRes}: $currentProgress")
+ Timber.v("--- ${leaf().nameRes}: $currentProgress")
status.postValue(
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt
index 53967784a1..ae8e8ce891 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt
@@ -19,7 +19,8 @@ package im.vector.matrix.android.internal.session.filter
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.FilterEntity
import im.vector.matrix.android.internal.database.model.FilterEntityFields
-import im.vector.matrix.android.internal.database.query.getFilter
+import im.vector.matrix.android.internal.database.query.get
+import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.where
@@ -29,26 +30,28 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
- val filter = FilterEntity.getFilter(realm)
- val result = if (filter.filterBodyJson != filterBody.toJSONString()) {
- // Filter has changed, store it and reset the filter Id
- monarchy.awaitTransaction {
+ val filter = FilterEntity.get(realm)
+ // Filter has changed, or no filter Id yet
+ filter == null
+ || filter.filterBodyJson != filterBody.toJSONString()
+ || filter.filterId.isBlank()
+ }.also { hasChanged ->
+ if (hasChanged) {
+ // Filter is new or has changed, store it and reset the filter Id.
+ // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
+ monarchy.awaitTransaction { realm ->
// We manage only one filter for now
val filterBodyJson = filterBody.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString()
- val filterEntity = FilterEntity.getFilter(it)
+ val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterBodyJson
filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset filterId
filterEntity.filterId = ""
}
- true
- } else {
- filter.filterId.isBlank()
}
- result
}
}
@@ -67,7 +70,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun getFilter(): String {
return Realm.getInstance(monarchy.realmConfiguration).use {
- val filter = FilterEntity.getFilter(it)
+ val filter = FilterEntity.getOrCreate(it)
if (filter.filterId.isBlank()) {
// Use the Json format
filter.filterBodyJson
@@ -80,7 +83,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun getRoomFilter(): String {
return Realm.getInstance(monarchy.realmConfiguration).use {
- FilterEntity.getFilter(it).roomEventFilterJson
+ FilterEntity.getOrCreate(it).roomEventFilterJson
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
index 224b3bcfeb..3d7c5df5fc 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
@@ -298,7 +298,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
} else {
- Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}")
+ Timber.e("Unknown relation type ${content.relatesTo?.type} for event ${event.eventId}")
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
index 7e5de176bb..b9dca748cb 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
@@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.LocalEcho
-import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.*
@@ -57,22 +56,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
override suspend fun execute(params: SetReadMarkersTask.Params) {
val markers = HashMap()
- val fullyReadEventId: String?
- val readReceiptEventId: String?
Timber.v("Execute set read marker with params: $params")
- if (params.markAllAsRead) {
+ val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
}
- fullyReadEventId = latestSyncedEventId
- readReceiptEventId = latestSyncedEventId
+ Pair(latestSyncedEventId, latestSyncedEventId)
} else {
- fullyReadEventId = params.fullyReadEventId
- readReceiptEventId = params.readReceiptEventId
+ Pair(params.fullyReadEventId, params.readReceiptEventId)
}
- if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
+ if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event $fullyReadEventId")
} else {
@@ -118,16 +113,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
}
}
}
-
- private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean {
- return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
- val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId
- ?: return true
- val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst()
- val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst()
- val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
- val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE
- newReadMarkerIndex > currentReadMarkerIndex
- }
- }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
index 11be821d7e..db3b6100a0 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
@@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
override fun editTextMessage(targetEventId: String,
msgType: String,
- newBodyText: String,
+ newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String): Cancelable {
val event = eventFactory
@@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.executeBy(taskExecutor)
}
- override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
+ override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
?.also { saveLocalEcho(it) }
?: return null
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
index 7c720e56a7..8fad03b588 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
@@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
- override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
+ override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
saveLocalEcho(it)
}
@@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
return sendEvent(event)
}
- override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
- val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also {
+ override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
+ val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
saveLocalEcho(it)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
index 3fa0dcdca1..b773d1f892 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
@@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
+import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
@@ -50,45 +51,55 @@ import javax.inject.Inject
*
* The transactionID is used as loc
*/
-internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String,
- private val stringProvider: StringProvider,
- private val roomSummaryUpdater: RoomSummaryUpdater) {
+internal class LocalEchoEventFactory @Inject constructor(
+ @UserId private val userId: String,
+ private val stringProvider: StringProvider,
+ private val roomSummaryUpdater: RoomSummaryUpdater,
+ private val textPillsUtils: TextPillsUtils
+) {
// TODO Inject
private val parser = Parser.builder().build()
// TODO Inject
private val renderer = HtmlRenderer.builder().build()
- fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
- if (msgType == MessageType.MSGTYPE_TEXT) {
- return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
+ fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
+ if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
+ return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
}
- val content = MessageTextContent(type = msgType, body = text)
+ val content = MessageTextContent(type = msgType, body = text.toString())
return createEvent(roomId, content)
}
- private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent {
+ private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) {
- val document = parser.parse(text)
+ val source = textPillsUtils.processSpecialSpansToMarkdown(text)
+ ?: text.toString()
+ val document = parser.parse(source)
val htmlText = renderer.render(document)
- if (isFormattedTextPertinent(text, htmlText)) {
- return TextContent(text, htmlText)
+ if (isFormattedTextPertinent(source, htmlText)) {
+ return TextContent(source, htmlText)
+ }
+ } else {
+ // Try to detect pills
+ textPillsUtils.processSpecialSpansToHtml(text)?.let {
+ return TextContent(text.toString(), it)
}
}
- return TextContent(text)
+ return TextContent(text.toString())
}
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "${text.trim()}
\n"
- fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
- return createEvent(roomId, textContent.toMessageTextContent())
+ fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
+ return createEvent(roomId, textContent.toMessageTextContent(msgType))
}
fun createReplaceTextEvent(roomId: String,
targetEventId: String,
- newBodyText: String,
+ newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
@@ -279,7 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
return System.currentTimeMillis()
}
- fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? {
+ fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? {
// Fallbacks and event representation
// TODO Add error/warning logs when any of this is null
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
@@ -298,7 +309,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
//
// > <@alice:example.org> This is the original body
//
- val replyFallback = buildReplyFallback(body, userId, replyText)
+ val replyFallback = buildReplyFallback(body, userId, replyText.toString())
val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent(
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt
new file mode 100644
index 0000000000..5ad61b5441
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.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.matrix.android.internal.session.room.send.pills
+
+import im.vector.matrix.android.api.session.room.send.UserMentionSpan
+
+internal data class MentionLinkSpec(
+ val span: UserMentionSpan,
+ val start: Int,
+ val end: Int
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt
new file mode 100644
index 0000000000..76fd8336cc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.room.send.pills
+
+import javax.inject.Inject
+
+internal class MentionLinkSpecComparator @Inject constructor() : Comparator {
+
+ override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int {
+ return when {
+ o1.start < o2.start -> -1
+ o1.start > o2.start -> 1
+ o1.end < o2.end -> 1
+ o1.end > o2.end -> -1
+ else -> 0
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt
new file mode 100644
index 0000000000..580e49b2ce
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.room.send.pills
+
+import android.text.SpannableString
+import im.vector.matrix.android.api.session.room.send.UserMentionSpan
+import java.util.*
+import javax.inject.Inject
+
+/**
+ * Utility class to detect special span in CharSequence and turn them into
+ * formatted text to send them as a Matrix messages.
+ *
+ * For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
+ */
+internal class TextPillsUtils @Inject constructor(
+ private val mentionLinkSpecComparator: MentionLinkSpecComparator
+) {
+
+ /**
+ * Detects if transformable spans are present in the text.
+ * @return the transformed String or null if no Span found
+ */
+ fun processSpecialSpansToHtml(text: CharSequence): String? {
+ return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
+ }
+
+ /**
+ * Detects if transformable spans are present in the text.
+ * @return the transformed String or null if no Span found
+ */
+ fun processSpecialSpansToMarkdown(text: CharSequence): String? {
+ return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
+ }
+
+ private fun transformPills(text: CharSequence, template: String): String? {
+ val spannableString = SpannableString.valueOf(text)
+ val pills = spannableString
+ ?.getSpans(0, text.length, UserMentionSpan::class.java)
+ ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
+ ?.toMutableList()
+ ?.takeIf { it.isNotEmpty() }
+ ?: return null
+
+ // we need to prune overlaps!
+ pruneOverlaps(pills)
+
+ return buildString {
+ var currIndex = 0
+ pills.forEachIndexed { _, (urlSpan, start, end) ->
+ // We want to replace with the pill with a html link
+ // append text before pill
+ append(text, currIndex, start)
+ // append the pill
+ append(String.format(template, urlSpan.userId, urlSpan.displayName))
+ currIndex = end
+ }
+ // append text after the last pill
+ append(text, currIndex, text.length)
+ }
+ }
+
+ private fun pruneOverlaps(links: MutableList) {
+ Collections.sort(links, mentionLinkSpecComparator)
+ var len = links.size
+ var i = 0
+ while (i < len - 1) {
+ val a = links[i]
+ val b = links[i + 1]
+ var remove = -1
+
+ // test if there is an overlap
+ if (b.start in a.start until a.end) {
+ when {
+ b.end <= a.end ->
+ // b is inside a -> b should be removed
+ remove = i + 1
+ a.end - a.start > b.end - b.start ->
+ // overlap and a is bigger -> b should be removed
+ remove = i + 1
+ a.end - a.start < b.end - b.start ->
+ // overlap and a is smaller -> a should be removed
+ remove = i
+ }
+
+ if (remove != -1) {
+ links.removeAt(remove)
+ len--
+ continue
+ }
+ }
+ i++
+ }
+ }
+
+ companion object {
+ private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s"
+
+ private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt
index 96e1caf71b..08d34d3056 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt
@@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task {
- apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
+ apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
}
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
index 4127e43540..b83240a681 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
@@ -74,22 +74,14 @@ internal class DefaultTimeline(
private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
- private val hiddenReadReceipts: TimelineHiddenReadReceipts,
- private val hiddenReadMarker: TimelineHiddenReadMarker
-) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
+ private val hiddenReadReceipts: TimelineHiddenReadReceipts
+) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
}
- override var listener: Timeline.Listener? = null
- set(value) {
- field = value
- BACKGROUND_HANDLER.post {
- postSnapshot()
- }
- }
-
+ private val listeners = ArrayList()
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler()
@@ -110,7 +102,7 @@ internal class DefaultTimeline(
private val backwardsState = AtomicReference(State())
private val forwardsState = AtomicReference(State())
- private val timelineID = UUID.randomUUID().toString()
+ override val timelineID = UUID.randomUUID().toString()
override val isLive
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
@@ -197,7 +189,6 @@ internal class DefaultTimeline(
if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
}
- hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this)
isReady.set(true)
}
}
@@ -217,7 +208,6 @@ internal class DefaultTimeline(
if (this::filteredEvents.isInitialized) {
filteredEvents.removeAllChangeListeners()
}
- hiddenReadMarker.dispose()
if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose()
}
@@ -298,7 +288,21 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
-// TimelineHiddenReadReceipts.Delegate
+ override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
+ listeners.add(listener).also {
+ postSnapshot()
+ }
+ }
+
+ override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
+ listeners.remove(listener)
+ }
+
+ override fun removeAllListeners() = synchronized(listeners) {
+ listeners.clear()
+ }
+
+ // TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List): Boolean {
return rebuildEvent(eventId) { te ->
@@ -310,19 +314,7 @@ internal class DefaultTimeline(
postSnapshot()
}
-// TimelineHiddenReadMarker.Delegate
-
- override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
- return rebuildEvent(eventId) { te ->
- te.copy(hasReadMarker = hasReadMarker)
- }
- }
-
- override fun onReadMarkerUpdated() {
- postSnapshot()
- }
-
-// Private methods *****************************************************************************
+ // Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex ->
@@ -502,9 +494,9 @@ internal class DefaultTimeline(
return
}
val params = PaginationTask.Params(roomId = roomId,
- from = token,
- direction = direction.toPaginationDirection(),
- limit = limit)
+ from = token,
+ direction = direction.toPaginationDirection(),
+ limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
@@ -579,7 +571,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted()
- && timelineEvent.root.mxDecryptionResult == null) {
+ && timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
}
@@ -641,7 +633,7 @@ internal class DefaultTimeline(
}
private fun fetchEvent(eventId: String) {
- val params = GetContextOfEventTask.Params(roomId, eventId)
+ val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
@@ -652,7 +644,13 @@ internal class DefaultTimeline(
}
updateLoadingStates(filteredEvents)
val snapshot = createSnapshot()
- val runnable = Runnable { listener?.onUpdated(snapshot) }
+ val runnable = Runnable {
+ synchronized(listeners) {
+ listeners.forEach {
+ it.onUpdated(snapshot)
+ }
+ }
+ }
debouncer.debounce("post_snapshot", runnable, 50)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
index 3bd67d38c3..d92dbd66be 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
@@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline(roomId,
- eventId,
- monarchy.realmConfiguration,
- taskExecutor,
- contextOfEventTask,
- clearUnlinkedEventsTask,
- paginationTask,
- cryptoService,
- timelineEventMapper,
- settings,
- TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
- TimelineHiddenReadMarker(roomId, settings)
+ eventId,
+ monarchy.realmConfiguration,
+ taskExecutor,
+ contextOfEventTask,
+ clearUnlinkedEventsTask,
+ paginationTask,
+ cryptoService,
+ timelineEventMapper,
+ settings,
+ TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
index 4dfe3e5c45..f06697351e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
@@ -30,6 +30,7 @@ data class EventContextResponse(
@Json(name = "state") override val stateEvents: List = emptyList()
) : TokenChunkEvent {
- override val events: List
- get() = listOf(event)
+ override val events: List by lazy {
+ eventsAfter.reversed() + listOf(event) + eventsBefore
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt
deleted file mode 100644
index 4f80883bf9..0000000000
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt
+++ /dev/null
@@ -1,133 +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.session.room.timeline
-
-import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
-import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
-import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
-import im.vector.matrix.android.internal.database.model.TimelineEventEntity
-import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
-import im.vector.matrix.android.internal.database.query.FilterContent
-import im.vector.matrix.android.internal.database.query.where
-import io.realm.OrderedRealmCollectionChangeListener
-import io.realm.Realm
-import io.realm.RealmQuery
-import io.realm.RealmResults
-
-/**
- * This class is responsible for handling the read marker for hidden events.
- * When an hidden event has read marker, we want to transfer it on the first older displayed event.
- * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
- */
-internal class TimelineHiddenReadMarker constructor(private val roomId: String,
- private val settings: TimelineSettings) {
-
- interface Delegate {
- fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
- fun onReadMarkerUpdated()
- }
-
- private var previousDisplayedEventId: String? = null
- private var hiddenReadMarker: RealmResults? = null
-
- private lateinit var filteredEvents: RealmResults
- private lateinit var nonFilteredEvents: RealmResults
- private lateinit var delegate: Delegate
-
- private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet ->
- if (!readMarkers.isLoaded || !readMarkers.isValid) {
- return@OrderedRealmCollectionChangeListener
- }
- var hasChange = false
- if (changeSet.deletions.isNotEmpty()) {
- previousDisplayedEventId?.also {
- hasChange = delegate.rebuildEvent(it, false)
- previousDisplayedEventId = null
- }
- }
- val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener
- val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
- ?: return@OrderedRealmCollectionChangeListener
-
- val isLoaded = nonFilteredEvents.where()
- .equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId)
- .findFirst() != null
-
- val displayIndex = hiddenEvent.root?.displayIndex
- if (isLoaded && displayIndex != null) {
- // Then we are looking for the first displayable event after the hidden one
- val firstDisplayedEvent = filteredEvents.where()
- .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
- .findFirst()
-
- // If we find one, we should rebuild this one with marker
- if (firstDisplayedEvent != null) {
- previousDisplayedEventId = firstDisplayedEvent.eventId
- hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
- }
- }
- if (hasChange) {
- delegate.onReadMarkerUpdated()
- }
- }
-
- /**
- * Start the realm query subscription. Has to be called on an HandlerThread
- */
- fun start(realm: Realm,
- filteredEvents: RealmResults,
- nonFilteredEvents: RealmResults,
- delegate: Delegate) {
- this.filteredEvents = filteredEvents
- this.nonFilteredEvents = nonFilteredEvents
- this.delegate = delegate
- // We are looking for read receipts set on hidden events.
- // We only accept those with a timelineEvent (so coming from pagination/sync).
- hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId)
- .isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT)
- .filterReceiptsWithSettings()
- .findAllAsync()
- .also { it.addChangeListener(readMarkerListener) }
- }
-
- /**
- * Dispose the realm query subscription. Has to be called on an HandlerThread
- */
- fun dispose() {
- this.hiddenReadMarker?.removeAllChangeListeners()
- }
-
- /**
- * We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
- */
- private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery {
- beginGroup()
- if (settings.filterTypes) {
- not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
- }
- if (settings.filterTypes && settings.filterEdits) {
- or()
- }
- if (settings.filterEdits) {
- like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
- }
- endGroup()
- return this
- }
-}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
index fbeabff0b5..7bff2936fd 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
@@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.signout
import android.content.Context
+import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
@@ -27,6 +28,8 @@ import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.worker.WorkManagerUtil
+import io.realm.Realm
+import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -42,6 +45,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@UserCacheDirectory private val userFile: File,
private val realmKeysUtils: RealmKeysUtils,
+ @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
+ @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
@@ -71,5 +76,15 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
+
+ // Sanity check
+ if (BuildConfig.DEBUG) {
+ Realm.getGlobalInstanceCount(realmSessionConfiguration)
+ .takeIf { it > 0 }
+ ?.let { Timber.e("All realm instance for session has not been closed ($it)") }
+ Realm.getGlobalInstanceCount(realmCryptoConfiguration)
+ .takeIf { it > 0 }
+ ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt
index 853774460f..61ae8b9925 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt
@@ -16,14 +16,10 @@
package im.vector.matrix.android.internal.session.sync
-import im.vector.matrix.android.internal.database.model.EventEntity
-import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
-import im.vector.matrix.android.internal.database.model.TimelineEventEntity
-import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.getOrCreate
-import im.vector.matrix.android.internal.database.query.where
+import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() {
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId
}
- // Remove the old markers if any
- val oldReadMarkerEvents = TimelineEventEntity
- .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
- .isNotNull(TimelineEventEntityFields.READ_MARKER.`$`)
- .findAll()
-
- oldReadMarkerEvents.forEach { it.readMarker = null }
- val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
+ ReadMarkerEntity.getOrCreate(realm, roomId).apply {
this.eventId = content.eventId
}
- // Attach to timelineEvent if known
- val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll()
- timelineEventEntities.forEach { it.readMarker = readMarkerEntity }
}
}
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 e425d53a62..d77f669215 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -15,7 +15,7 @@ androidExtensions {
}
ext.versionMajor = 0
-ext.versionMinor = 8
+ext.versionMinor = 9
ext.versionPatch = 0
static def getGitTimestamp() {
@@ -221,10 +221,11 @@ dependencies {
def arrow_version = "0.8.2"
def coroutines_version = "1.3.2"
def markwon_version = '4.1.2'
- def big_image_viewer_version = '1.5.6'
+ def big_image_viewer_version = '1.6.2'
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/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt
index daf432fb45..6804828b20 100644
--- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt
+++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt
@@ -29,7 +29,7 @@ class SasEmojiController : TypedEpoxyController() {
if (data == null) return
data.emojiList.forEachIndexed { idx, emojiRepresentation ->
- itemSasEmoji {
+ sasEmojiItem {
id(idx)
index(idx)
emojiRepresentation(emojiRepresentation)
diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt
similarity index 96%
rename from vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt
rename to vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt
index 92d9bc0b11..cf35873f6b 100644
--- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt
+++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt
@@ -25,7 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji)
-abstract class ItemSasEmoji : VectorEpoxyModel() {
+abstract class SasEmojiItem : VectorEpoxyModel() {
@EpoxyAttribute
var index: Int = 0
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/epoxy/bottomsheet/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt
similarity index 97%
rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt
rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt
index 483650a434..c55dbdde8a 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt
@@ -37,7 +37,7 @@ import im.vector.riotx.features.themes.ThemeUtils
* A action for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
-abstract class BottomSheetItemAction : VectorEpoxyModel() {
+abstract class BottomSheetActionItem : VectorEpoxyModel() {
@EpoxyAttribute
@DrawableRes
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
similarity index 83%
rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt
rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
index 999068b289..8105d7a7c0 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
@@ -16,6 +16,7 @@
*/
package im.vector.riotx.core.epoxy.bottomsheet
+import android.text.method.MovementMethod
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@@ -25,12 +26,13 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess
/**
* A message preview for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
-abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() {
+abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() {
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@@ -44,11 +46,15 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() {
+abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel() {
@EpoxyAttribute
lateinit var fontProvider: EmojiCompatFontProvider
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
similarity index 95%
rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt
rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
index 9b9d0fc380..1a5b4e2f66 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
@@ -31,7 +31,7 @@ import im.vector.riotx.features.home.AvatarRenderer
* A room preview for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview)
-abstract class BottomSheetItemRoomPreview : VectorEpoxyModel() {
+abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel() {
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt
similarity index 94%
rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt
rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt
index 08d727cfa9..8f830ba706 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt
@@ -30,7 +30,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
* A send state for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
-abstract class BottomSheetItemSendState : VectorEpoxyModel() {
+abstract class BottomSheetSendStateItem : VectorEpoxyModel() {
@EpoxyAttribute
var showProgress: Boolean = false
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt
similarity index 90%
rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt
rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt
index fddf507bf9..dd41d5dd66 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt
@@ -22,7 +22,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
-abstract class BottomSheetItemSeparator : VectorEpoxyModel() {
+abstract class BottomSheetSeparatorItem : VectorEpoxyModel() {
class Holder : VectorEpoxyHolder()
}
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 2dc75c5fa2..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,11 +17,19 @@
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"
+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/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
index 387105c480..388ec9bebe 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
@@ -24,7 +24,3 @@ fun TimelineEvent.canReact(): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted()
}
-
-fun TimelineEvent.displayReadMarker(myUserId: String): Boolean {
- return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null
-}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt
index bcbab97360..41f98ed264 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt
@@ -21,6 +21,14 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
fun View.hideKeyboard() {
- val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(windowToken, 0)
+ val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ imm?.hideSoftInputFromWindow(windowToken, 0)
+}
+
+fun View.showKeyboard(andRequestFocus: Boolean = false) {
+ if (andRequestFocus) {
+ requestFocus()
+ }
+ val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
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/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
index abc2dd98f8..b2adde449a 100644
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
@@ -23,7 +23,6 @@ import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
-import androidx.core.view.isInvisible
import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
@@ -34,7 +33,7 @@ class JumpToReadMarkerView @JvmOverloads constructor(
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
- fun onJumpToReadMarkerClicked(readMarkerId: String)
+ fun onJumpToReadMarkerClicked()
fun onClearReadMarkerClicked()
}
@@ -44,24 +43,15 @@ class JumpToReadMarkerView @JvmOverloads constructor(
setupView()
}
- private var readMarkerId: String? = null
-
private fun setupView() {
inflate(context, R.layout.view_jump_to_read_marker, this)
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
jumpToReadMarkerLabelView.setOnClickListener {
- readMarkerId?.also {
- callback?.onJumpToReadMarkerClicked(it)
- }
+ callback?.onJumpToReadMarkerClicked()
}
closeJumpToReadMarkerView.setOnClickListener {
visibility = View.INVISIBLE
callback?.onClearReadMarkerClicked()
}
}
-
- fun render(show: Boolean, readMarkerId: String?) {
- this.readMarkerId = readMarkerId
- isInvisible = !show
- }
}
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt
deleted file mode 100644
index 0fb8b55250..0000000000
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt
+++ /dev/null
@@ -1,89 +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.riotx.core.ui.views
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.view.animation.Animation
-import android.view.animation.AnimationUtils
-import im.vector.riotx.R
-import kotlinx.coroutines.*
-
-private const val DELAY_IN_MS = 1_000L
-
-class ReadMarkerView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : View(context, attrs, defStyleAttr) {
-
- interface Callback {
- fun onReadMarkerLongBound(isDisplayed: Boolean)
- }
-
- private var eventId: String? = null
- private var callback: Callback? = null
- private var callbackDispatcherJob: Job? = null
-
- fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) {
- this.eventId = eventId
- this.callback = readMarkerCallback
- if (displayReadMarker) {
- startAnimation()
- } else {
- this.animation?.cancel()
- this.visibility = INVISIBLE
- }
- if (hasReadMarker) {
- callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
- delay(DELAY_IN_MS)
- callback?.onReadMarkerLongBound(displayReadMarker)
- }
- }
- }
-
- fun unbind() {
- this.callbackDispatcherJob?.cancel()
- this.callback = null
- this.eventId = null
- this.animation?.cancel()
- this.visibility = INVISIBLE
- }
-
- private fun startAnimation() {
- if (animation == null) {
- animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim)
- animation.startOffset = DELAY_IN_MS / 2
- animation.duration = DELAY_IN_MS / 2
- animation.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation) {
- }
-
- override fun onAnimationEnd(animation: Animation) {
- visibility = INVISIBLE
- }
-
- override fun onAnimationRepeat(animation: Animation) {}
- })
- }
- visibility = VISIBLE
- animation.start()
- }
-}
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/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
index 3f5808949b..bc451f8e84 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
@@ -27,7 +27,7 @@ object CommandParser {
* @param textMessage the text message
* @return a parsed slash command (ok or error)
*/
- fun parseSplashCommand(textMessage: String): ParsedCommand {
+ fun parseSplashCommand(textMessage: CharSequence): ParsedCommand {
// check if it has the Slash marker
if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand
@@ -76,7 +76,7 @@ object CommandParser {
}
}
Command.EMOTE.command -> {
- val message = textMessage.substring(Command.EMOTE.command.length).trim()
+ val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim()
ParsedCommand.SendEmote(message)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
index 02f5abe540..89438c8a9d 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
@@ -33,7 +33,7 @@ sealed class ParsedCommand {
// Valid commands:
- class SendEmote(val message: String) : ParsedCommand()
+ class SendEmote(val message: CharSequence) : ParsedCommand()
class BanUser(val userId: String, val reason: String) : ParsedCommand()
class UnbanUser(val userId: String) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
index 104aa301cb..1102c67e16 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
@@ -97,7 +97,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
if (status == null) {
waiting_view.isVisible = false
} else {
- Timber.e("${getString(status.statusText)} ${status.percentProgress}")
+ Timber.v("${getString(status.statusText)} ${status.percentProgress}")
waiting_view.setOnClickListener {
// block interactions
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt
index cf6abf12e9..59f31ec2ee 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt
@@ -16,10 +16,8 @@
package im.vector.riotx.features.home.createdirect
-import android.content.Context
import android.os.Bundle
import android.view.View
-import android.view.inputmethod.InputMethodManager
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
@@ -27,6 +25,7 @@ import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch
+import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
import javax.inject.Inject
@@ -63,9 +62,7 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString()))
}
.disposeOnDestroyView()
- createDirectRoomSearchById.requestFocus()
- val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
- imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT)
+ createDirectRoomSearchById.showKeyboard(andRequestFocus = true)
}
private fun setupCloseView() {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt
deleted file mode 100644
index 7b3ebeb71c..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt
+++ /dev/null
@@ -1,99 +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.riotx.features.home.room.detail
-
-import androidx.recyclerview.widget.LinearLayoutManager
-import im.vector.riotx.core.di.ScreenScope
-import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
-import javax.inject.Inject
-
-@ScreenScope
-class ReadMarkerHelper @Inject constructor() {
-
- lateinit var timelineEventController: TimelineEventController
- lateinit var layoutManager: LinearLayoutManager
- var callback: Callback? = null
-
- private var onReadMarkerLongDisplayed = false
- private var jumpToReadMarkerVisible = false
- private var readMarkerVisible: Boolean = true
- private var state: RoomDetailViewState? = null
-
- fun readMarkerVisible(): Boolean {
- return readMarkerVisible
- }
-
- fun onResume() {
- onReadMarkerLongDisplayed = false
- }
-
- fun onReadMarkerLongDisplayed() {
- onReadMarkerLongDisplayed = true
- }
-
- fun updateWith(newState: RoomDetailViewState) {
- state = newState
- checkReadMarkerVisibility()
- checkJumpToReadMarkerVisibility()
- }
-
- fun onTimelineScrolled() {
- checkJumpToReadMarkerVisibility()
- }
-
- private fun checkReadMarkerVisibility() {
- val nonNullState = this.state ?: return
- val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
- val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
- readMarkerVisible = if (!onReadMarkerLongDisplayed) {
- true
- } else {
- if (nonNullState.timeline?.isLive == false) {
- true
- } else {
- !(firstVisibleItem == 0 && lastVisibleItem > 0)
- }
- }
- }
-
- private fun checkJumpToReadMarkerVisibility() {
- val nonNullState = this.state ?: return
- val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
- val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId
- val newJumpToReadMarkerVisible = if (readMarkerId == null) {
- false
- } else {
- val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId)
- ?: readMarkerId
- val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId)
- if (positionOfReadMarker == null) {
- nonNullState.timeline?.isLive == true && lastVisibleItem > 0
- } else {
- positionOfReadMarker > lastVisibleItem
- }
- }
- if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) {
- jumpToReadMarkerVisible = newJumpToReadMarkerVisible
- callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId)
- }
- }
-
- interface Callback {
- fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?)
- }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
index 2e59e70d08..c1743ae3fc 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
@@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomDetailAction : VectorViewModelAction {
data class SaveDraft(val draft: String) : RoomDetailAction()
- data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailAction()
+ data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
data class SendMedia(val attachments: List) : RoomDetailAction()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
@@ -35,13 +35,15 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction()
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction()
- data class SetReadMarkerAction(val eventId: String) : RoomDetailAction()
object MarkAllAsRead : RoomDetailAction()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction()
data class HandleTombstoneEvent(val event: Event) : RoomDetailAction()
object AcceptInvite : RoomDetailAction()
object RejectInvite : RoomDetailAction()
+ object EnterTrackingUnreadMessagesState : RoomDetailAction()
+ object ExitTrackingUnreadMessagesState : RoomDetailAction()
+
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index 6fdbf94590..d50b0c9f68 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
-import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.ColorDrawable
@@ -29,7 +28,6 @@ import android.os.Parcelable
import android.text.Editable
import android.text.Spannable
import android.view.*
-import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
@@ -37,9 +35,11 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
+import androidx.core.text.buildSpannedString
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.forEach
+import androidx.core.view.isVisible
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -58,7 +58,6 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState
@@ -73,6 +72,7 @@ import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
+import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment
@@ -145,8 +145,7 @@ class RoomDetailFragment @Inject constructor(
val textComposerViewModelFactory: TextComposerViewModel.Factory,
private val errorFormatter: ErrorFormatter,
private val eventHtmlRenderer: EventHtmlRenderer,
- private val vectorPreferences: VectorPreferences,
- private val readMarkerHelper: ReadMarkerHelper
+ private val vectorPreferences: VectorPreferences
) :
VectorBaseFragment(),
TimelineEventController.Callback,
@@ -158,7 +157,7 @@ class RoomDetailFragment @Inject constructor(
companion object {
- /**x
+ /**
* Sanitize the display name.
*
* @param displayName the display name to sanitize
@@ -292,6 +291,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onDestroy() {
+ roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
debouncer.cancelAll()
super.onDestroy()
}
@@ -299,6 +299,7 @@ class RoomDetailFragment @Inject constructor(
private fun setupJumpToBottomView() {
jumpToBottomView.visibility = View.INVISIBLE
jumpToBottomView.setOnClickListener {
+ roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
jumpToBottomView.visibility = View.INVISIBLE
withState(roomDetailViewModel) { state ->
if (state.timeline?.isLive == false) {
@@ -405,7 +406,12 @@ class RoomDetailFragment @Inject constructor(
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
- avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar)
+ avatarRenderer.render(
+ event.senderAvatar,
+ event.root.senderId ?: "",
+ event.getDisambiguatedDisplayName(),
+ composerLayout.composerRelatedMessageAvatar
+ )
composerLayout.expand {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
@@ -418,12 +424,12 @@ class RoomDetailFragment @Inject constructor(
if (text != composerLayout.composerEditText.text.toString()) {
// Ignore update to avoid saving a draft
composerLayout.composerEditText.setText(text)
- composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
+ composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
+ ?: 0)
}
}
override fun onResume() {
- readMarkerHelper.onResume()
super.onResume()
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
}
@@ -468,24 +474,12 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback)
- }
- readMarkerHelper.timelineEventController = timelineEventController
- readMarkerHelper.layoutManager = layoutManager
- readMarkerHelper.callback = object : ReadMarkerHelper.Callback {
- override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) {
- jumpToReadMarkerView.render(show, readMarkerId)
- }
+ updateJumpToReadMarkerViewVisibility()
+ updateJumpToBottomViewVisibility()
}
recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
- override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
- updateJumpToBottomViewVisibility()
- }
- readMarkerHelper.onTimelineScrolled()
- }
-
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
@@ -527,6 +521,30 @@ class RoomDetailFragment @Inject constructor(
}
}
+ private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
+ withState(roomDetailViewModel) {
+ val showJumpToUnreadBanner = when (it.unreadState) {
+ UnreadState.Unknown,
+ UnreadState.HasNoUnread -> false
+ is UnreadState.ReadMarkerNotLoaded -> true
+ is UnreadState.HasUnread -> {
+ if (it.canShowJumpToReadMarker) {
+ val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
+ val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
+ if (positionOfReadMarker == null) {
+ false
+ } else {
+ positionOfReadMarker > lastVisibleItem
+ }
+ } else {
+ false
+ }
+ }
+ }
+ jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
+ }
+ }
+
private fun updateJumpToBottomViewVisibility() {
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
@@ -588,7 +606,13 @@ class RoomDetailFragment @Inject constructor(
// Add the span
val user = session.getUser(item.userId)
- val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user)
+ val span = PillImageSpan(
+ glideRequests,
+ avatarRenderer,
+ requireContext(),
+ item.userId,
+ user?.displayName ?: item.userId,
+ user?.avatarUrl)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -609,7 +633,7 @@ class RoomDetailFragment @Inject constructor(
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
}
- override fun onSendMessage(text: String) {
+ override fun onSendMessage(text: CharSequence) {
if (lockSendButton) {
Timber.w("Send button is locked")
return
@@ -651,13 +675,12 @@ class RoomDetailFragment @Inject constructor(
}
private fun renderState(state: RoomDetailViewState) {
- readMarkerHelper.updateWith(state)
renderRoomSummary(state)
val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
scrollOnHighlightedEventCallback.timeline = state.timeline
- timelineEventController.update(state, readMarkerHelper.readMarkerVisible())
+ timelineEventController.update(state)
inviteView.visibility = View.GONE
val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
@@ -975,9 +998,8 @@ class RoomDetailFragment @Inject constructor(
vectorBaseActivity.notImplemented("Click on user avatar")
}
- @SuppressLint("SetTextI18n")
override fun onMemberNameClicked(informationData: MessageInformationData) {
- insertUserDisplayNameInTextEditor(informationData.memberName?.toString())
+ insertUserDisplayNameInTextEditor(informationData.senderId)
}
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
@@ -1014,28 +1036,9 @@ class RoomDetailFragment @Inject constructor(
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
}
- override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) {
- readMarkerHelper.onReadMarkerLongDisplayed()
- val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return
- val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
- if (readMarkerIndex > lastVisibleItemPosition) {
- return
- }
- val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
- var nextReadMarkerId: String? = null
- for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) {
- val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition)
- if (timelineItem is BaseEventItem) {
- val eventId = timelineItem.getEventIds().firstOrNull() ?: continue
- if (!LocalEcho.isLocalEchoId(eventId)) {
- nextReadMarkerId = eventId
- break
- }
- }
- }
- if (nextReadMarkerId != null) {
- roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId))
- }
+ override fun onReadMarkerVisible() {
+ updateJumpToReadMarkerViewVisibility()
+ roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
}
// AutocompleteUserPresenter.Callback
@@ -1153,61 +1156,73 @@ class RoomDetailFragment @Inject constructor(
is EventSharedAction.IgnoreUser -> {
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
}
+ is EventSharedAction.OnUrlClicked -> {
+ onUrlClicked(action.url)
+ }
+ is EventSharedAction.OnUrlLongClicked -> {
+ onUrlLongClicked(action.url)
+ }
else -> {
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
}
}
}
-// utils
/**
- * Insert an user displayname in the message editor.
+ * Insert a user displayName in the message editor.
*
- * @param text the text to insert.
+ * @param userId the userId.
*/
-// TODO legacy, refactor
- private fun insertUserDisplayNameInTextEditor(text: String?) {
- // TODO move logic outside of fragment
- if (null != text) {
-// var vibrate = false
+ @SuppressLint("SetTextI18n")
+ private fun insertUserDisplayNameInTextEditor(userId: String) {
+ val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
- val myDisplayName = session.getUser(session.myUserId)?.displayName
- if (myDisplayName == text) {
- // current user
- if (composerLayout.composerEditText.text.isNullOrBlank()) {
- composerLayout.composerEditText.append(Command.EMOTE.command + " ")
- composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
-// vibrate = true
- }
- } else {
- // another user
- if (composerLayout.composerEditText.text.isNullOrBlank()) {
- // Ensure displayName will not be interpreted as a Slash command
- if (text.startsWith("/")) {
- composerLayout.composerEditText.append("\\")
+ if (startToCompose
+ && userId == session.myUserId) {
+ // Empty composer, current user: start an emote
+ composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
+ composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1)
+ } else {
+ val roomMember = roomDetailViewModel.getMember(userId)
+ // TODO move logic outside of fragment
+ (roomMember?.displayName ?: userId)
+ .let { sanitizeDisplayName(it) }
+ .let { displayName ->
+ buildSpannedString {
+ append(displayName)
+ setSpan(
+ PillImageSpan(
+ glideRequests,
+ avatarRenderer,
+ requireContext(),
+ userId,
+ displayName,
+ roomMember?.avatarUrl)
+ .also { it.bind(composerLayout.composerEditText) },
+ 0,
+ displayName.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ append(if (startToCompose) ": " else " ")
+ }.let { pill ->
+ if (startToCompose) {
+ if (displayName.startsWith("/")) {
+ // Ensure displayName will not be interpreted as a Slash command
+ composerLayout.composerEditText.append("\\")
+ }
+ composerLayout.composerEditText.append(pill)
+ } else {
+ composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, pill)
+ }
+ }
}
- composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
- } else {
- composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
- }
-
-// vibrate = true
- }
-
-// if (vibrate && vectorPreferences.vibrateWhenMentioning()) {
-// val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
-// if (v?.hasVibrator() == true) {
-// v.vibrate(100)
-// }
-// }
- focusComposerAndShowKeyboard()
}
+
+ focusComposerAndShowKeyboard()
}
private fun focusComposerAndShowKeyboard() {
- composerLayout.composerEditText.requestFocus()
- val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
- imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
+ composerLayout.composerEditText.showKeyboard(andRequestFocus = true)
}
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
@@ -1230,8 +1245,14 @@ class RoomDetailFragment @Inject constructor(
// JumpToReadMarkerView.Callback
- override fun onJumpToReadMarkerClicked(readMarkerId: String) {
- roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false))
+ override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
+ jumpToReadMarkerView.isVisible = false
+ if (it.unreadState is UnreadState.HasUnread) {
+ roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
+ }
+ if (it.unreadState is UnreadState.ReadMarkerNotLoaded) {
+ roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false))
+ }
}
override fun onClearReadMarkerClicked() {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index d2c2c7fdde..642bce3319 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.jakewharton.rxrelay2.BehaviorRelay
+import com.jakewharton.rxrelay2.PublishRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@@ -34,11 +35,15 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.room.model.Membership
+import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.send.UserDraft
+import im.vector.matrix.android.api.session.room.timeline.Timeline
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@@ -57,19 +62,23 @@ import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.settings.VectorPreferences
+import io.reactivex.Observable
+import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
+import io.reactivex.schedulers.Schedulers
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
private val session: Session
-) : VectorViewModel(initialState) {
+) : VectorViewModel(initialState), Timeline.Listener {
private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId
@@ -89,6 +98,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
}
+ private var timelineEvents = PublishRelay.create>()
private var timeline = room.createTimeline(eventId, timelineSettings)
// Can be used for several actions, for a one shot result
@@ -101,6 +111,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// Slot to keep a pending uri during permission request
var pendingUri: Uri? = null
+ private var trackUnreadMessages = AtomicBoolean(false)
+ private var mostRecentDisplayedEvent: TimelineEvent? = null
+
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
@@ -119,52 +132,74 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
init {
+ getUnreadState()
observeSyncState()
observeRoomSummary()
observeEventDisplayedActions()
observeSummaryState()
observeDrafts()
+ observeUnreadState()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
+ timeline.addListener(this)
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
}
override fun handle(action: RoomDetailAction) {
when (action) {
- is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
- is RoomDetailAction.SendMessage -> handleSendMessage(action)
- is RoomDetailAction.SendMedia -> handleSendMedia(action)
- is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
- is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
- is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
- is RoomDetailAction.SendReaction -> handleSendReaction(action)
- is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
- is RoomDetailAction.RejectInvite -> handleRejectInvite()
- is RoomDetailAction.RedactAction -> handleRedactEvent(action)
- is RoomDetailAction.UndoReaction -> handleUndoReact(action)
- is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
- is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
- is RoomDetailAction.EnterEditMode -> handleEditAction(action)
- is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
- is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
- is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
- is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
- is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
- is RoomDetailAction.ResendMessage -> handleResendEvent(action)
- is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
- is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
- is RoomDetailAction.ResendAll -> handleResendAll()
- is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action)
- is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
- is RoomDetailAction.ReportContent -> handleReportContent(action)
- is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
+ is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
+ is RoomDetailAction.SendMessage -> handleSendMessage(action)
+ is RoomDetailAction.SendMedia -> handleSendMedia(action)
+ is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
+ is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
+ is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
+ is RoomDetailAction.SendReaction -> handleSendReaction(action)
+ is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
+ is RoomDetailAction.RejectInvite -> handleRejectInvite()
+ is RoomDetailAction.RedactAction -> handleRedactEvent(action)
+ is RoomDetailAction.UndoReaction -> handleUndoReact(action)
+ is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
+ is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
+ is RoomDetailAction.EnterEditMode -> handleEditAction(action)
+ is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
+ is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
+ is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
+ is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
+ is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
+ is RoomDetailAction.ResendMessage -> handleResendEvent(action)
+ is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
+ is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
+ is RoomDetailAction.ResendAll -> handleResendAll()
+ is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
+ is RoomDetailAction.ReportContent -> handleReportContent(action)
+ is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
+ is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
+ is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
}
}
+ private fun startTrackingUnreadMessages() {
+ trackUnreadMessages.set(true)
+ setState { copy(canShowJumpToReadMarker = false) }
+ }
+
+ private fun stopTrackingUnreadMessages() {
+ if (trackUnreadMessages.getAndSet(false)) {
+ mostRecentDisplayedEvent?.root?.eventId?.also {
+ room.setReadMarker(it, callback = object : MatrixCallback {})
+ }
+ mostRecentDisplayedEvent = null
+ }
+ setState { copy(canShowJumpToReadMarker = true) }
+ }
+
private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) {
invisibleEventsObservable.accept(action)
}
+ fun getMember(userId: String) : RoomMember? {
+ return room.getRoomMember(userId)
+ }
/**
* Convert a send mode to a draft and save the draft
*/
@@ -355,7 +390,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (inReplyTo != null) {
// TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let {
- room.editReply(state.sendMode.timelineEvent, it, action.text)
+ room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
}
} else {
val messageContent: MessageContent? =
@@ -380,7 +415,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body
- val finalText = legacyRiotQuoteText(textMsg, action.text)
+ val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
+
+ // TODO check for pills?
// TODO Refactor this, just temporary for quotes
val parser = Parser.builder().build()
@@ -397,7 +434,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
- room.replyToMessage(it, action.text, action.autoMarkdown)
+ room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft()
}
@@ -621,6 +658,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
+ stopTrackingUnreadMessages()
val targetEventId: String = action.eventId
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
@@ -679,26 +717,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions ->
- val mostRecentEvent = actions.maxBy { it.event.displayIndex }
- mostRecentEvent?.event?.root?.eventId?.let { eventId ->
+ val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
+ ?: return@subscribeBy
+ val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
+ if (trackUnreadMessages.get()) {
+ if (globalMostRecentDisplayedEvent == null) {
+ mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
+ } else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) {
+ mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
+ }
+ }
+ bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback {})
}
})
.disposeOnClear()
}
- private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState {
- var readMarkerId = action.eventId
- val indexOfEvent = timeline.getIndexOfEvent(readMarkerId)
- // force to set the read marker on the next event
- if (indexOfEvent != null) {
- timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
- readMarkerId = eventIdOfNext
- }
- }
- room.setReadMarker(readMarkerId, callback = object : MatrixCallback {})
- }
-
private fun handleMarkAllAsRead() {
room.markAllAsRead(object : MatrixCallback {})
}
@@ -753,6 +788,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
+ private fun getUnreadState() {
+ Observable
+ .combineLatest, RoomSummary, UnreadState>(
+ timelineEvents.observeOn(Schedulers.computation()),
+ room.rx().liveRoomSummary().unwrap(),
+ BiFunction { timelineEvents, roomSummary ->
+ computeUnreadState(timelineEvents, roomSummary)
+ }
+ )
+ // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
+ .distinctUntilChanged { previous, current ->
+ when {
+ previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
+ current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
+ else -> false
+ }
+ }
+ .subscribe {
+ setState { copy(unreadState = it) }
+ }
+ .disposeOnClear()
+ }
+
+ private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState {
+ if (events.isEmpty()) return UnreadState.Unknown
+ val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
+ val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
+ ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
+ val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
+ ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
+ for (i in (firstDisplayableEventIndex - 1) downTo 0) {
+ val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
+ val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
+ val isFromMe = timelineEvent.root.senderId == session.myUserId
+ if (!isFromMe) {
+ return UnreadState.HasUnread(eventId)
+ }
+ }
+ return UnreadState.HasNoUnread
+ }
+
+ private fun observeUnreadState() {
+ selectSubscribe(RoomDetailViewState::unreadState) {
+ Timber.v("Unread state: $it")
+ if (it is UnreadState.HasNoUnread) {
+ startTrackingUnreadMessages()
+ }
+ }
+ }
+
private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
if (summary.membership == Membership.INVITE) {
@@ -768,8 +853,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
+ override fun onUpdated(snapshot: List) {
+ timelineEvents.accept(snapshot)
+ }
+
override fun onCleared() {
timeline.dispose()
+ timeline.removeAllListeners()
super.onCleared()
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index 03110858a1..a0be8fc9dc 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -41,6 +41,13 @@ sealed class SendMode(open val text: String) {
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
}
+sealed class UnreadState {
+ object Unknown : UnreadState()
+ object HasNoUnread : UnreadState()
+ data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState()
+ data class HasUnread(val firstUnreadEventId: String) : UnreadState()
+}
+
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
@@ -52,7 +59,9 @@ data class RoomDetailViewState(
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async = Uninitialized,
val syncState: SyncState = SyncState.IDLE,
- val highlightedEventId: String? = null
+ val highlightedEventId: String? = null,
+ val unreadState: UnreadState = UnreadState.Unknown,
+ val canShowJumpToReadMarker: Boolean = true
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
index 273aeecbfa..ab37431103 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
@@ -20,12 +20,17 @@ package im.vector.riotx.features.home.room.detail.composer
import android.content.Context
import android.net.Uri
import android.os.Build
+import android.text.Editable
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
+import im.vector.riotx.core.extensions.ooi
+import im.vector.riotx.core.platform.SimpleTextWatcher
+import im.vector.riotx.features.html.PillImageSpan
+import timber.log.Timber
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
: AppCompatEditText(context, attrs, defStyleAttr) {
@@ -55,4 +60,41 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib
}
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
}
+
+ init {
+ addTextChangedListener(
+ object : SimpleTextWatcher() {
+ var spanToRemove: PillImageSpan? = null
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after")
+
+ if (count > after) {
+ // A char has been deleted
+ val deleteCharPosition = start + count
+ Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition")
+
+ // Get the first span at this position
+ spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java)
+ .ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") }
+ .firstOrNull()
+ }
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ if (spanToRemove != null) {
+ val start = editableText.getSpanStart(spanToRemove)
+ val end = editableText.getSpanEnd(spanToRemove)
+ Timber.v("Pills: afterTextChanged Removing the span start:$start end:$end")
+ // Must be done before text replacement
+ editableText.removeSpan(spanToRemove)
+ if (start != -1 && end != -1) {
+ editableText.replace(start, end, "")
+ }
+ spanToRemove = null
+ }
+ }
+ }
+ )
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
index 32307dc3d4..593ce1a8f6 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
@@ -26,6 +26,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.text.toSpannable
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
@@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
interface Callback : ComposerEditText.Callback {
fun onCloseRelatedMessage()
- fun onSendMessage(text: String)
+ fun onSendMessage(text: CharSequence)
fun onAddAttachment()
}
@@ -86,7 +87,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
}
sendButton.setOnClickListener {
- val textMessage = text?.toString() ?: ""
+ val textMessage = text?.toSpannable() ?: ""
callback?.onSendMessage(textMessage)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
index 50ade56474..f220570e69 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
@@ -66,7 +66,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.adapter = epoxyController.adapter
- bottomSheetTitle.text = getString(R.string.read_at)
+ bottomSheetTitle.text = getString(R.string.seen_by)
epoxyController.setData(displayReadReceiptArgs.readReceipts)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
index be2f1dd7e4..326e19c431 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
@@ -25,21 +25,18 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime
-import im.vector.riotx.core.utils.DimensionConverter
-import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
+import im.vector.riotx.features.home.room.detail.UnreadState
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
-import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
-import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
-import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
-import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
+import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@@ -47,11 +44,10 @@ import org.threeten.bp.LocalDateTime
import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
+ private val session: Session,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
- private val avatarRenderer: AvatarRenderer,
- private val dimensionConverter: DimensionConverter,
@TimelineEventControllerHandler
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
@@ -86,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List)
- fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean)
+ fun onReadMarkerVisible()
}
interface UrlClickCallback {
@@ -101,6 +97,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private var currentSnapshot: List = emptyList()
private var inSubmitList: Boolean = false
private var timeline: Timeline? = null
+ private var unreadState: UnreadState = UnreadState.Unknown
+ private var positionOfReadMarker: Int? = null
+ private var eventIdToHighlight: String? = null
var callback: Callback? = null
@@ -152,7 +151,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
// Update position when we are building new items
- override fun intercept(models: MutableList>) {
+ override fun intercept(models: MutableList>) = synchronized(modelCache) {
+ positionOfReadMarker = null
adapterPositionMapping.clear()
models.forEachIndexed { index, epoxyModel ->
if (epoxyModel is BaseEventItem) {
@@ -161,18 +161,25 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
}
+ val currentUnreadState = this.unreadState
+ if (currentUnreadState is UnreadState.HasUnread) {
+ val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
+ positionOfReadMarker = position
+ if (position != null) {
+ val readMarker = TimelineReadMarkerItem_()
+ .also {
+ it.id("read_marker")
+ it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
+ }
+ models.add(position, readMarker)
+ }
+ }
}
- fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) {
- if (timeline != viewState.timeline) {
+ fun update(viewState: RoomDetailViewState) {
+ if (timeline?.timelineID != viewState.timeline?.timelineID) {
timeline = viewState.timeline
- timeline?.listener = this
- // Clear cache
- synchronized(modelCache) {
- for (i in 0 until modelCache.size) {
- modelCache[i] = null
- }
- }
+ timeline?.addListener(this)
}
var requestModelBuild = false
if (eventIdToHighlight != viewState.highlightedEventId) {
@@ -188,8 +195,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
eventIdToHighlight = viewState.highlightedEventId
requestModelBuild = true
}
- if (this.readMarkerVisible != readMarkerVisible) {
- this.readMarkerVisible = readMarkerVisible
+ if (this.unreadState != viewState.unreadState) {
+ this.unreadState = viewState.unreadState
requestModelBuild = true
}
if (requestModelBuild) {
@@ -197,9 +204,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
- private var readMarkerVisible: Boolean = false
- private var eventIdToHighlight: String? = null
-
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
timelineMediaSizeProvider.recyclerView = recyclerView
@@ -224,7 +228,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
- // Timeline.LISTENER ***************************************************************************
+// Timeline.LISTENER ***************************************************************************
override fun onUpdated(snapshot: List) {
submitSnapshot(snapshot)
@@ -246,43 +250,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
private fun getModels(): List> {
- synchronized(modelCache) {
- (0 until modelCache.size).forEach { position ->
- // Should be build if not cached or if cached but contains mergedHeader or formattedDay
- // We then are sure we always have items up to date.
- if (modelCache[position] == null
- || modelCache[position]?.mergedHeaderModel != null
- || modelCache[position]?.formattedDayModel != null) {
- modelCache[position] = buildItemModels(position, currentSnapshot)
- }
- }
- return modelCache
- .map {
- val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
- null
- } else {
- it.eventModel
- }
- listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
+ buildCacheItemsIfNeeded()
+ return modelCache
+ .map {
+ val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
+ null
+ } else {
+ it.eventModel
}
- .flatten()
- .filterNotNull()
+ listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
+ }
+ .flatten()
+ .filterNotNull()
+ }
+
+ private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
+ if (modelCache.isEmpty()) {
+ return
+ }
+ (0 until modelCache.size).forEach { position ->
+ // Should be build if not cached or if cached but contains additional models
+ // We then are sure we always have items up to date.
+ if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) {
+ modelCache[position] = buildCacheItem(position, currentSnapshot)
+ }
}
}
- private fun buildItemModels(currentPosition: Int, items: List): CacheItemData {
+ private fun buildCacheItem(currentPosition: Int, items: List): CacheItemData {
val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition)
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
- // Don't show read marker if it's on first item
- val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) {
- false
- } else {
- readMarkerVisible
- }
- val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also {
+ val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
@@ -290,7 +291,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
nextEvent = nextEvent,
items = items,
addDaySeparator = addDaySeparator,
- readMarkerVisible = readMarkerVisible,
currentPosition = currentPosition,
eventIdToHighlight = eventIdToHighlight,
callback = callback
@@ -298,7 +298,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
requestModelBuild()
}
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
-
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
}
@@ -335,6 +334,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return adapterPositionMapping[eventId]
}
+ fun getPositionOfReadMarker(): Int? = synchronized(modelCache) {
+ return positionOfReadMarker
+ }
+
fun isLoadingForward() = showingForwardLoader
private data class CacheItemData(
@@ -343,5 +346,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null
- )
+ ) {
+ fun shouldTriggerBuild(): Boolean {
+ return mergedHeaderModel != null || formattedDayModel != null
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt
index 37d96ad62c..8077786d06 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt
@@ -88,4 +88,12 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
+
+ // An url in the event preview has been clicked
+ data class OnUrlClicked(val url: String) :
+ EventSharedAction(0, 0)
+
+ // An url in the event preview has been long clicked
+ data class OnUrlLongClicked(val url: String) :
+ EventSharedAction(0, 0)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
index 3f4171f733..a5bf6f8558 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
@@ -68,6 +68,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
messageActionsEpoxyController.listener = this
}
+ override fun onUrlClicked(url: String): Boolean {
+ sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
+ // Always consume
+ return true
+ }
+
+ override fun onUrlLongClicked(url: String): Boolean {
+ sharedActionViewModel.post(EventSharedAction.OnUrlLongClicked(url))
+ // Always consume
+ return true
+ }
+
override fun didSelectMenuAction(eventAction: EventSharedAction) {
if (eventAction is EventSharedAction.ReportContent) {
// Toggle report menu
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index b561a6df3c..efbfd3434c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -23,6 +23,9 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.bottomsheet.*
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
+import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import javax.inject.Inject
/**
@@ -38,26 +41,27 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
// Message preview
val body = state.messageBody
if (body != null) {
- bottomSheetItemMessagePreview {
+ bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
avatarUrl(state.informationData.avatarUrl ?: "")
senderId(state.informationData.senderId)
senderName(state.senderName())
- body(body)
+ movementMethod(createLinkMovementMethod(listener))
+ body(body.linkify(listener))
time(state.time())
}
}
// Send state
if (state.informationData.sendState.isSending()) {
- bottomSheetItemSendState {
+ bottomSheetSendStateItem {
id("send_state")
showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message))
}
} else if (state.informationData.sendState.hasFailed()) {
- bottomSheetItemSendState {
+ bottomSheetSendStateItem {
id("send_state")
showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message))
@@ -68,16 +72,16 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
// Quick reactions
if (state.canReact() && state.quickStates is Success) {
// Separator
- bottomSheetItemSeparator {
+ bottomSheetSeparatorItem {
id("reaction_separator")
}
- bottomSheetItemQuickReactions {
+ bottomSheetQuickReactionsItem {
id("quick_reaction")
fontProvider(fontProvider)
texts(state.quickStates()?.map { it.reaction }.orEmpty())
selecteds(state.quickStates.invoke().map { it.isSelected })
- listener(object : BottomSheetItemQuickReactions.Listener {
+ listener(object : BottomSheetQuickReactionsItem.Listener {
override fun didSelect(emoji: String, selected: Boolean) {
listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected))
}
@@ -86,18 +90,18 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
}
// Separator
- bottomSheetItemSeparator {
+ bottomSheetSeparatorItem {
id("actions_separator")
}
// Action
state.actions()?.forEachIndexed { index, action ->
if (action is EventSharedAction.Separator) {
- bottomSheetItemSeparator {
+ bottomSheetSeparatorItem {
id("separator_$index")
}
} else {
- bottomSheetItemAction {
+ bottomSheetActionItem {
id("action_$index")
iconRes(action.iconResId)
textRes(action.titleRes)
@@ -114,7 +118,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId),
EventSharedAction.ReportContentCustom(action.eventId, action.senderId)
).forEachIndexed { indexReport, actionReport ->
- bottomSheetItemAction {
+ bottomSheetActionItem {
id("actionReport_$indexReport")
subMenuItem(true)
iconRes(actionReport.iconResId)
@@ -127,7 +131,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
}
}
- interface MessageActionsEpoxyControllerListener {
+ interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback {
fun didSelectMenuAction(eventAction: EventSharedAction)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
index 1ae47f9c22..94d7812512 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
@@ -46,7 +46,6 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
fun create(event: TimelineEvent,
highlight: Boolean,
- readMarkerVisible: Boolean,
callback: TimelineEventController.Callback?,
exception: Exception? = null): DefaultItem {
val text = if (exception == null) {
@@ -54,7 +53,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
} else {
"an exception occurred when rendering the event ${event.root.eventId}"
}
- val informationData = informationDataFactory.create(event, null, readMarkerVisible)
+ val informationData = informationDataFactory.create(event, null)
return create(text, informationData, highlight, callback)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
index e67507d7bb..512fffa29e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
@@ -25,9 +25,10 @@ import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
-import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
+import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import me.gujun.android.span.span
import javax.inject.Inject
@@ -41,7 +42,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
highlight: Boolean,
- readMarkerVisible: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
event.root.eventId ?: return null
@@ -57,7 +57,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
}
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
- ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
+ ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = span(message) {
textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
@@ -65,14 +65,14 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
// TODO This is not correct format for error, change it
- val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible)
+ val informationData = messageInformationDataFactory.create(event, nextEvent)
val attributes = attributesFactory.create(null, informationData, callback)
return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.attributes(attributes)
.message(spannableStr)
- .urlClickCallback(callback)
+ .movementMethod(createLinkMovementMethod(callback))
}
else -> null
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 51364e24c9..a2e979a08d 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -36,7 +36,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
nextEvent: TimelineEvent?,
items: List,
addDaySeparator: Boolean,
- readMarkerVisible: Boolean,
currentPosition: Int,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?,
@@ -50,20 +49,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
null
} else {
var highlighted = false
- var readMarkerId: String? = null
- var showReadMarker = false
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = ArrayList(mergedEvents.size)
mergedEvents.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
- if (readMarkerId == null && mergedEvent.hasReadMarker) {
- readMarkerId = mergedEvent.root.eventId
- }
- if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) {
- showReadMarker = true
- }
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = MergedHeaderItem.Data(
@@ -96,8 +87,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
},
- readMarkerId = readMarkerId,
- showReadMarker = isCollapsed && showReadMarker,
readReceiptsCallback = callback
)
MergedHeaderItem_()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index ac6c563099..9c96f17022 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -24,8 +24,6 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.view.View
import dagger.Lazy
-import im.vector.matrix.android.api.permalinks.MatrixLinkify
-import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
@@ -35,7 +33,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
-import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
@@ -45,8 +42,10 @@ import im.vector.riotx.core.utils.isLocalFile
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.*
-import im.vector.riotx.features.html.EventHtmlRenderer
+import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
+import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import im.vector.riotx.features.html.CodeVisitor
+import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import me.gujun.android.span.span
@@ -70,12 +69,11 @@ class MessageItemFactory @Inject constructor(
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
highlight: Boolean,
- readMarkerVisible: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
event.root.eventId ?: return null
- val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible)
+ val informationData = messageInformationDataFactory.create(event, nextEvent)
if (event.root.isRedacted()) {
// message is redacted
@@ -89,10 +87,10 @@ class MessageItemFactory @Inject constructor(
return defaultItemFactory.create(malformedText, informationData, highlight, callback)
}
if (messageContent.relatesTo?.type == RelationType.REPLACE
- || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE
+ || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE
) {
// This is an edit event, we should it when debugging as a notice event
- return noticeItemFactory.create(event, highlight, readMarkerVisible, callback)
+ return noticeItemFactory.create(event, highlight, callback)
}
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
@@ -195,8 +193,7 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
- url = messageContent.videoInfo?.thumbnailFile?.url
- ?: messageContent.videoInfo?.thumbnailUrl,
+ url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
@@ -258,7 +255,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
- val linkifiedBody = linkifyBody(body, callback)
+ val linkifiedBody = body.linkify(callback)
return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
@@ -273,7 +270,7 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
- .urlClickCallback(callback)
+ .movementMethod(createLinkMovementMethod(callback))
}
private fun buildCodeBlockItem(formattedBody: CharSequence,
@@ -326,9 +323,9 @@ class MessageItemFactory @Inject constructor(
// nop
}
},
- editStart,
- editEnd,
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+ editStart,
+ editEnd,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable
}
@@ -344,14 +341,14 @@ class MessageItemFactory @Inject constructor(
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textStyle = "italic"
}
- linkifyBody(formattedBody, callback)
+ formattedBody.linkify(callback)
}
return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.message(message)
.highlighted(highlight)
- .urlClickCallback(callback)
+ .movementMethod(createLinkMovementMethod(callback))
}
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
@@ -361,7 +358,7 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let {
val formattedBody = "* ${informationData.memberName} $it"
- linkifyBody(formattedBody, callback)
+ formattedBody.linkify(callback)
}
return MessageTextItem_()
.apply {
@@ -375,7 +372,7 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
- .urlClickCallback(callback)
+ .movementMethod(createLinkMovementMethod(callback))
}
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
@@ -386,17 +383,6 @@ class MessageItemFactory @Inject constructor(
.highlighted(highlight)
}
- private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
- val spannable = SpannableStringBuilder(body)
- MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
- override fun onUrlClicked(url: String) {
- callback?.onUrlClicked(url)
- }
- })
- VectorLinkify.addLinks(spannable, true)
- return spannable
- }
-
companion object {
private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
index 8768da26cf..4ee90f82a9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
@@ -34,10 +34,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
fun create(event: TimelineEvent,
highlight: Boolean,
- readMarkerVisible: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
- val informationData = informationDataFactory.create(event, null, readMarkerVisible)
+ val informationData = informationDataFactory.create(event, null)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,
informationData = informationData,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 618ca121c2..5b6dec9900 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -33,14 +33,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
- readMarkerVisible: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try {
when (event.root.getClearType()) {
EventType.STICKER,
- EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
+ EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
// State and call
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
@@ -53,21 +52,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_ANSWER,
EventType.REACTION,
EventType.REDACTION,
- EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback)
+ EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
- messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
+ messageItemFactory.create(event, nextEvent, highlight, callback)
} else {
- encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
+ encryptedItemFactory.create(event, nextEvent, highlight, callback)
}
}
// Unhandled event types (yet)
- EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
else -> {
Timber.v("Type ${event.root.getClearType()} not handled")
null
@@ -75,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
}
} catch (e: Exception) {
Timber.e(e, "failed to create message item")
- defaultItemFactory.create(event, highlight, readMarkerVisible, callback, e)
+ defaultItemFactory.create(event, highlight, callback, e)
}
return (computedModel ?: EmptyItem_())
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index e44e657733..784a180d00 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -39,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
private val dateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider) {
- fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData {
+ fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before
val eventId = event.root.eventId!!
@@ -47,7 +47,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
- ?: false
+ ?: false
val showInformation =
addDaySeparator
@@ -63,8 +63,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
}
- val displayReadMarker = readMarkerVisible && event.hasReadMarker
-
return MessageInformationData(
eventId = eventId,
senderId = event.root.senderId ?: "",
@@ -88,9 +86,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
- .toList(),
- hasReadMarker = event.hasReadMarker,
- displayReadMarker = displayReadMarker
+ .toList()
)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
similarity index 85%
rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt
rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
index c2aaf482ae..69b2b24899 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
@@ -21,6 +21,16 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?)
+ : VectorEpoxyModel.OnVisibilityStateChangedListener {
+
+ override fun onVisibilityStateChanged(visibilityState: Int) {
+ if (visibilityState == VisibilityState.VISIBLE) {
+ callback?.onReadMarkerVisible()
+ }
+ }
+}
+
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
private val event: TimelineEvent)
: VectorEpoxyModel.OnVisibilityStateChangedListener {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 2ca6bbfd37..713b60d4d8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
-import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@@ -50,13 +49,6 @@ abstract class AbsMessageItem : BaseEventItem() {
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
- private val _readMarkerCallback = object : ReadMarkerView.Callback {
-
- override fun onReadMarkerLongBound(isDisplayed: Boolean) {
- attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
- }
- }
-
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
@@ -110,12 +102,6 @@ abstract class AbsMessageItem : BaseEventItem() {
attributes.avatarRenderer,
_readReceiptsClickListener
)
- holder.readMarkerView.bindView(
- attributes.informationData.eventId,
- attributes.informationData.hasReadMarker,
- attributes.informationData.displayReadMarker,
- _readMarkerCallback
- )
val reactions = attributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
@@ -138,7 +124,6 @@ abstract class AbsMessageItem : BaseEventItem() {
}
override fun unbind(holder: H) {
- holder.readMarkerView.unbind()
holder.readReceiptsView.unbind()
super.unbind(holder)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
index 8543484b00..02b7341c72 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
@@ -19,14 +19,12 @@ import android.view.View
import android.view.ViewStub
import android.widget.RelativeLayout
import androidx.annotation.IdRes
-import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.platform.CheckableView
-import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DimensionConverter
@@ -62,7 +60,6 @@ abstract class BaseEventItem : VectorEpoxyModel
val leftGuideline by bind(R.id.messageStartGuideline)
val checkableBackground by bind(R.id.messageSelectedBackground)
val readReceiptsView by bind(R.id.readReceiptsView)
- val readMarkerView by bind(R.id.readMarkerView)
override fun bindView(itemView: View) {
super.bindView(itemView)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
index 01e82ddf6b..a2a3c9ad3b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
@@ -25,7 +25,6 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
-import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@@ -39,13 +38,6 @@ abstract class MergedHeaderItem : BaseEventItem() {
attributes.mergeData.distinctBy { it.userId }
}
- private val _readMarkerCallback = object : ReadMarkerView.Callback {
-
- override fun onReadMarkerLongBound(isDisplayed: Boolean) {
- attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed)
- }
- }
-
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
@@ -77,20 +69,14 @@ abstract class MergedHeaderItem : BaseEventItem() {
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
- holder.readMarkerView.bindView(
- attributes.readMarkerId,
- !attributes.readMarkerId.isNullOrEmpty(),
- attributes.showReadMarker,
- _readMarkerCallback)
- }
-
- override fun unbind(holder: Holder) {
- holder.readMarkerView.unbind()
- super.unbind(holder)
}
override fun getEventIds(): List {
- return attributes.mergeData.map { it.eventId }
+ return if (attributes.isCollapsed) {
+ attributes.mergeData.map { it.eventId }
+ } else {
+ emptyList()
+ }
}
data class Data(
@@ -102,9 +88,7 @@ abstract class MergedHeaderItem : BaseEventItem() {
)
data class Attributes(
- val readMarkerId: String?,
val isCollapsed: Boolean,
- val showReadMarker: Boolean,
val mergeData: List,
val avatarRenderer: AvatarRenderer,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
@@ -119,6 +103,6 @@ abstract class MergedHeaderItem : BaseEventItem() {
}
companion object {
- private const val STUB_ID = R.id.messageContentMergedheaderStub
+ private const val STUB_ID = R.id.messageContentMergedHeaderStub
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
index 96c74ccb88..2dd581ce6f 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -33,9 +33,7 @@ data class MessageInformationData(
val orderedReactionList: List? = null,
val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false,
- val readReceipts: List = emptyList(),
- val hasReadMarker: Boolean = false,
- val displayReadMarker: Boolean = false
+ val readReceipts: List = emptyList()
) : Parcelable
@Parcelize
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
index 45a6e2e743..5ee0576be7 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -16,22 +16,14 @@
package im.vector.riotx.features.home.room.detail.timeline.item
-import android.view.MotionEvent
+import android.text.method.MovementMethod
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat
-import androidx.core.text.toSpannable
import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
-import im.vector.riotx.core.utils.isValidUrl
-import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
-import im.vector.riotx.features.html.PillImageSpan
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import me.saket.bettermovementmethod.BetterLinkMovementMethod
+import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageTextItem : AbsMessageItem() {
@@ -43,30 +35,11 @@ abstract class MessageTextItem : AbsMessageItem() {
@EpoxyAttribute
var useBigFont: Boolean = false
@EpoxyAttribute
- var urlClickCallback: TimelineEventController.UrlClickCallback? = null
-
- // Better link movement methods fixes the issue when
- // long pressing to open the context menu on a TextView also triggers an autoLink click.
- private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
- it.setOnLinkClickListener { _, url ->
- // Return false to let android manage the click on the link, or true if the link is handled by the application
- url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true
- }
- // We need also to fix the case when long click on link will trigger long click on cell
- it.setOnLinkLongClickListener { tv, url ->
- // Long clicks are handled by parent, return true to block android to do something with url
- if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) {
- tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
- true
- } else {
- false
- }
- }
- }
+ var movementMethod: MovementMethod? = null
override fun bind(holder: Holder) {
super.bind(holder)
- holder.messageView.movementMethod = mvmtMethod
+ holder.messageView.movementMethod = movementMethod
if (useBigFont) {
holder.messageView.textSize = 44F
} else {
@@ -76,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem() {
holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
if (searchForPills) {
- findPillsAndProcess { it.bind(holder.messageView) }
+ message?.findPillsAndProcess { it.bind(holder.messageView) }
}
val textFuture = PrecomputedTextCompat.getTextFuture(
message ?: "",
@@ -85,17 +58,6 @@ abstract class MessageTextItem : AbsMessageItem() {
holder.messageView.setTextFuture(textFuture)
}
- private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
- GlobalScope.launch(Dispatchers.Main) {
- val pillImageSpans: Array? = withContext(Dispatchers.IO) {
- message?.toSpannable()?.let { spannable ->
- spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
- }
- }
- pillImageSpans?.forEach { processBlock(it) }
- }
- }
-
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
index 1f39ae3ca4..05dedcfa22 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
@@ -22,7 +22,6 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
-import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@@ -37,13 +36,6 @@ abstract class NoticeItem : BaseEventItem() {
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
- private val _readMarkerCallback = object : ReadMarkerView.Callback {
-
- override fun onReadMarkerLongBound(isDisplayed: Boolean) {
- attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
- }
- }
-
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = attributes.noticeText
@@ -56,17 +48,6 @@ abstract class NoticeItem : BaseEventItem() {
)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
- holder.readMarkerView.bindView(
- attributes.informationData.eventId,
- attributes.informationData.hasReadMarker,
- attributes.informationData.displayReadMarker,
- _readMarkerCallback
- )
- }
-
- override fun unbind(holder: Holder) {
- holder.readMarkerView.unbind()
- super.unbind(holder)
}
override fun getEventIds(): List {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt
new file mode 100644
index 0000000000..4d867156d3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.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.riotx.features.home.room.detail.timeline.item
+
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+
+@EpoxyModelClass(layout = R.layout.item_timeline_read_marker)
+abstract class TimelineReadMarkerItem : VectorEpoxyModel() {
+
+ override fun bind(holder: Holder) {
+ }
+
+ class Holder : VectorEpoxyHolder()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
new file mode 100644
index 0000000000..492248985e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.home.room.detail.timeline.tools
+
+import android.text.SpannableStringBuilder
+import android.view.MotionEvent
+import androidx.core.text.toSpannable
+import im.vector.matrix.android.api.permalinks.MatrixLinkify
+import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
+import im.vector.riotx.core.linkify.VectorLinkify
+import im.vector.riotx.core.utils.isValidUrl
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+import im.vector.riotx.features.html.PillImageSpan
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import me.saket.bettermovementmethod.BetterLinkMovementMethod
+
+fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) {
+ GlobalScope.launch(Dispatchers.Main) {
+ withContext(Dispatchers.IO) {
+ toSpannable().let { spannable ->
+ spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
+ }
+ }.forEach { processBlock(it) }
+ }
+}
+
+fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
+ val spannable = SpannableStringBuilder(this)
+ MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
+ override fun onUrlClicked(url: String) {
+ callback?.onUrlClicked(url)
+ }
+ })
+ VectorLinkify.addLinks(spannable, true)
+ return spannable
+}
+
+// Better link movement methods fixes the issue when
+// long pressing to open the context menu on a TextView also triggers an autoLink click.
+fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): BetterLinkMovementMethod {
+ return BetterLinkMovementMethod.newInstance()
+ .apply {
+ setOnLinkClickListener { _, url ->
+ // Return false to let android manage the click on the link, or true if the link is handled by the application
+ url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true
+ }
+
+ // We need also to fix the case when long click on link will trigger long click on cell
+ setOnLinkLongClickListener { tv, url ->
+ // Long clicks are handled by parent, return true to block android to do something with url
+ if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) {
+ tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
+ true
+ } else {
+ false
+ }
+ }
+ }
+}
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/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
index 74dab6563f..4107bf01b2 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
@@ -24,8 +24,8 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.helpFooterItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.resources.StringProvider
-import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.core.resources.UserPreferencesProvider
+import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem
import javax.inject.Inject
@@ -63,7 +63,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
RoomListDisplayMode.SHARE -> {
buildFilteredRooms(nonNullViewState)
}
- else -> {
+ else -> {
var showHelp = false
val roomSummaries = nonNullViewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
@@ -80,7 +80,10 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
nonNullViewState.joiningErrorRoomsIds,
nonNullViewState.rejectingRoomsIds,
nonNullViewState.rejectingErrorRoomsIds)
- showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
+ // Never set showHelp to true for invitation
+ if (category != RoomCategory.INVITE) {
+ showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
+ }
}
}
}
@@ -108,7 +111,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
when {
viewState.displayMode == RoomListDisplayMode.FILTERED -> addFilterFooter(viewState)
- filteredSummaries.isEmpty() -> addEmptyFooter()
+ filteredSummaries.isEmpty() -> addEmptyFooter()
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
index 2e17464cc6..84fd5bc6f2 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
@@ -18,9 +18,9 @@ package im.vector.riotx.features.home.room.list.actions
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
-import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemAction
-import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemRoomPreview
-import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemSeparator
+import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem
+import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem
+import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
@@ -36,7 +36,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
val roomSummary = state.roomSummary() ?: return
// Preview
- bottomSheetItemRoomPreview {
+ bottomSheetRoomPreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
roomName(roomSummary.displayName)
@@ -46,7 +46,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
}
// Notifications
- bottomSheetItemSeparator {
+ bottomSheetSeparatorItem {
id("notifications_separator")
}
@@ -57,7 +57,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState)
// Leave
- bottomSheetItemSeparator {
+ bottomSheetSeparatorItem {
id("leave_separator")
}
RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5)
@@ -72,7 +72,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
is RoomListQuickActionsSharedAction.Settings,
is RoomListQuickActionsSharedAction.Leave -> false
}
- return bottomSheetItemAction {
+ return bottomSheetActionItem {
id("action_$index")
selected(selected)
iconRes(iconResId)
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/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
index fdcbb12cd7..ecbf0da415 100644
--- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
+++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt
@@ -41,7 +41,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
- val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
+ val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user?.displayName
+ ?: permalinkData.userId, user?.avatarUrl)
SpannableBuilder.setSpans(
visitor.builder(),
span,
diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
index bc954204c0..a192c71961 100644
--- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
+++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
@@ -28,7 +28,7 @@ import androidx.annotation.UiThread
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable
-import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideRequests
import im.vector.riotx.features.home.AvatarRenderer
@@ -37,16 +37,14 @@ import java.lang.ref.WeakReference
/**
* This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
+ * Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills.
*/
class PillImageSpan(private val glideRequests: GlideRequests,
private val avatarRenderer: AvatarRenderer,
private val context: Context,
- private val userId: String,
- private val user: User?) : ReplacementSpan() {
-
- private val displayName by lazy {
- if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
- }
+ override val userId: String,
+ override val displayName: String,
+ private val avatarUrl: String?) : ReplacementSpan(), UserMentionSpan {
private val pillDrawable = createChipDrawable()
private val target = PillImageSpanTarget(this)
@@ -55,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
@UiThread
fun bind(textView: TextView) {
tv = WeakReference(textView)
- avatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target)
+ avatarRenderer.render(context, glideRequests, avatarUrl, userId, displayName, target)
}
// ReplacementSpan *****************************************************************************
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..e35923f5b0
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/Config.kt
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+const val MODULAR_LINK = "https://modular.im/?utm_source=riot-x-android&utm_medium=native&utm_campaign=riot-x-android-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..67935c1ae8 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,100 @@ 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)
+ loginTitle.text = getString(resId, "Modular")
+ 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 +176,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..0484357ae2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt
@@ -0,0 +1,103 @@
+/*
+ * 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
+ loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular)
+ 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