Merge branch 'release/0.11.0'

This commit is contained in:
Benoit Marty 2019-12-19 16:44:27 +01:00
commit 358fcb6b34
231 changed files with 7373 additions and 1795 deletions

View file

@ -18,6 +18,7 @@
<w>pbkdf</w> <w>pbkdf</w>
<w>pkcs</w> <w>pkcs</w>
<w>signin</w> <w>signin</w>
<w>signout</w>
<w>signup</w> <w>signup</w>
</words> </words>
</dictionary> </dictionary>

View file

@ -1,3 +1,27 @@
Changes in RiotX 0.11.0 (2019-12-19)
===================================================
Features ✨:
- Implement soft logout (#281)
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
- Limit sticker size in the timeline
Other changes:
- Use same default room colors than Riot-Web
Bugfix 🐛:
- Scroll breadcrumbs to top when opened
- Render default room name when it starts with an emoji (#477)
- Do not display " (IRC)" in display names https://github.com/vector-im/riot-android/issues/444
- Fix rendering issue with HTML formatted body
- Disable click on Stickers (#703)
Build 🧱:
- Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10) Changes in RiotX 0.10.0 (2019-12-10)
=================================================== ===================================================

View file

@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@ -45,12 +45,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google() google()
jcenter() jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
} }
tasks.withType(JavaCompile).all { tasks.withType(JavaCompile).all {

1
diff-match-patch/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,8 @@
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "8"
targetCompatibility = "8"

File diff suppressed because it is too large Load diff

View file

@ -1,38 +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.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.CompletableEmitter
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
override fun onSuccess(data: T) {
completableEmitter.onComplete()
}
override fun onFailure(failure: Throwable) {
completableEmitter.tryOnError(failure)
}
}
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
completableEmitter.setCancellable {
this.cancel()
}
}

View file

@ -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.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.Completable
import io.reactivex.Single
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onComplete()
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}

View file

@ -53,13 +53,13 @@ class RxRoom(private val room: Room) {
return room.getMyReadReceiptLive().asObservable() return room.getMyReadReceiptLive().asObservable()
} }
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create { fun loadRoomMembersIfNeeded(): Single<Unit> = singleBuilder {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) room.loadRoomMembersIfNeeded(it)
} }
fun joinRoom(reason: String? = null, fun joinRoom(reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create { viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) room.join(reason, viaServers, it)
} }
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> { fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {

View file

@ -66,20 +66,25 @@ class RxSession(private val session: Session) {
return session.livePagedUsers(filter).asObservable() return session.livePagedUsers(filter).asObservable()
} }
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create { fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) session.createRoom(roomParams, it)
} }
fun searchUsersDirectory(search: String, fun searchUsersDirectory(search: String,
limit: Int, limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create { excludedUserIds: Set<String>): Single<List<User>> = singleBuilder {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) session.searchUsersDirectory(search, limit, excludedUserIds, it)
} }
fun joinRoom(roomId: String, fun joinRoom(roomId: String,
reason: String? = null, reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create { viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) session.joinRoom(roomId, reason, viaServers, it)
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
} }
} }

View file

@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
*/ */
data class SessionParams( data class SessionParams(
val credentials: Credentials, val credentials: Credentials,
val homeServerConnectionConfig: HomeServerConnectionConfig val homeServerConnectionConfig: HomeServerConnectionConfig,
val isTokenValid: Boolean
) )

View file

@ -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.matrix.android.api.failure
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
}

View file

@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
/** /**
* This data class holds the error defined by the matrix specifications. * This data class holds the error defined by the matrix specifications.
* You shouldn't have to instantiate it. * You shouldn't have to instantiate it.
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
/** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String, @Json(name = "errcode") val code: String,
/** human-readable error message */
@Json(name = "error") val message: String, @Json(name = "error") val message: String,
// For M_CONSENT_NOT_GIVEN
@Json(name = "consent_uri") val consentUri: String? = null, @Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data // For M_RESOURCE_LIMIT_EXCEEDED
@Json(name = "limit_type") val limitType: String? = null, @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 // For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) { @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
) {
companion object { companion object {
const val FORBIDDEN = "M_FORBIDDEN" /** Forbidden access, e.g. joining a room without permission, failed login. */
const val UNKNOWN = "M_UNKNOWN" const val M_FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" /** An unknown error has occurred. */
const val MISSING_TOKEN = "M_MISSING_TOKEN" const val M_UNKNOWN = "M_UNKNOWN"
const val BAD_JSON = "M_BAD_JSON" /** The access token specified was not recognised. */
const val NOT_JSON = "M_NOT_JSON" const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val NOT_FOUND = "M_NOT_FOUND" /** No access token was specified for the request. */
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
const val USER_IN_USE = "M_USER_IN_USE" /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
const val ROOM_IN_USE = "M_ROOM_IN_USE" const val M_BAD_JSON = "M_BAD_JSON"
const val BAD_PAGINATION = "M_BAD_PAGINATION" /** Request did not contain valid JSON. */
const val UNAUTHORIZED = "M_UNAUTHORIZED" const val M_NOT_JSON = "M_NOT_JSON"
const val OLD_VERSION = "M_OLD_VERSION" /** No resource was found for this request. */
const val UNRECOGNIZED = "M_UNRECOGNIZED" const val M_NOT_FOUND = "M_NOT_FOUND"
/** Too many requests have been sent in a short period of time. Wait a while then try again. */
const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET" /* ==========================================================================================
const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" * Other error codes the client might encounter are
// Error code returned by the server when no account matches the given 3pid * ========================================================================================== */
const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
const val THREEPID_IN_USE = "M_THREEPID_IN_USE" /** Encountered when trying to register a user ID which has been taken. */
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" const val M_USER_IN_USE = "M_USER_IN_USE"
const val TOO_LARGE = "M_TOO_LARGE" /** Sent when the room alias given to the createRoom API is already in use. */
const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
/** (Not documented yet) */
const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
/** The request was not correctly authorized. Usually due to login failures. */
const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
/** (Not documented yet) */
const val M_OLD_VERSION = "M_OLD_VERSION"
/** The server did not understand the request. */
const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
/** (Not documented yet) */
const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
/** Authentication could not be performed on the third party identifier. */
const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
/** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
/** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
/** The client's request used a third party server, eg. identity server, that this server does not trust. */
const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
/** The request or entity was too large. */
const val M_TOO_LARGE = "M_TOO_LARGE"
/** (Not documented yet) */
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
* or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
* out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
* data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
/** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
/** Encountered when trying to register a user ID which is not valid. */
const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
/** Sent when the initial state given to the createRoom API is invalid. */
const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
/** The server does not permit this third party identifier. This may happen if the server only permits,
* for example, email addresses from a particular domain. */
const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
/** The client's request to create a room used a room version that the server does not support. */
const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
/** The client attempted to join a room that has a version the server does not support.
* Inspect the room_version property of the error response for the room's version. */
const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
/** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
const val M_BAD_STATE = "M_BAD_STATE"
/** The room or resource does not permit guests to access it. */
const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
/** A Captcha is required to complete the request. */
const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
/** The Captcha provided did not match what was expected. */
const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
/** A required parameter was missing from the request. */
const val M_MISSING_PARAM = "M_MISSING_PARAM"
/** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
const val M_INVALID_PARAM = "M_INVALID_PARAM"
/** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
const val M_EXCLUSIVE = "M_EXCLUSIVE"
/** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type" // Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user" const val LIMIT_TYPE_MAU = "monthly_active_user"

View file

@ -24,9 +24,7 @@ import android.net.Uri
*/ */
sealed class PermalinkData { sealed class PermalinkData {
data class EventLink(val roomIdOrAlias: String, val eventId: String) : PermalinkData() data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
data class RoomLink(val roomIdOrAlias: String) : PermalinkData()
data class UserLink(val userId: String) : PermalinkData() data class UserLink(val userId: String) : PermalinkData()

View file

@ -60,16 +60,21 @@ object PermalinkParser {
return PermalinkData.FallbackLink(uri) return PermalinkData.FallbackLink(uri)
} }
return when { return when {
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) { val eventId = extraParameter.takeIf {
PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter) !it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
} else {
PermalinkData.RoomLink(roomIdOrAlias = identifier)
} }
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
} }
else -> PermalinkData.FallbackLink(uri) MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
}
else -> PermalinkData.FallbackLink(uri)
} }
} }
} }

View file

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -62,6 +62,11 @@ interface Session :
*/ */
val sessionParams: SessionParams val sessionParams: SessionParams
/**
* The session is valid, i.e. it has a valid token so far
*/
val isOpenable: Boolean
/** /**
* Useful shortcut to get access to the userId * Useful shortcut to get access to the userId
*/ */
@ -81,7 +86,7 @@ interface Session :
/** /**
* Launches infinite periodic background syncs * Launches infinite periodic background syncs
* THis does not work in doze mode :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * If battery optimization is on it can work in app standby but that's all :/
*/ */
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
@ -136,13 +141,10 @@ interface Session :
*/ */
interface Listener { interface Listener {
/** /**
* The access token is not valid anymore * Possible cases:
* - The access token is not valid anymore,
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/ */
fun onInvalidToken() fun onGlobalError(globalError: GlobalError)
/**
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
} }
} }

View file

@ -30,7 +30,7 @@ data class ContentAttachmentData(
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null, val name: String? = null,
val path: String, val path: String,
val mimeType: String, val mimeType: String?,
val type: Type val type: Type
) : Parcelable { ) : Parcelable {

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@ -74,4 +75,11 @@ interface RoomService {
*/ */
fun markAllAsRead(roomIds: List<String>, fun markAllAsRead(roomIds: List<String>,
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable
/**
* Resolve a room alias to a room ID.
*/
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
} }

View file

@ -59,7 +59,6 @@ interface MembershipService {
/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */
fun join(reason: String? = null, fun join(reason: String? = null,
viaServers: List<String> = emptyList(), viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable

View file

@ -29,6 +29,8 @@ data class RoomSummary(
val displayName: String = "", val displayName: String = "",
val topic: String = "", val topic: String = "",
val avatarUrl: String = "", val avatarUrl: String = "",
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false, val isDirect: Boolean = false,
val latestPreviewableEvent: TimelineEvent? = null, val latestPreviewableEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(), val otherMemberIds: List<String> = emptyList(),

View file

@ -25,7 +25,7 @@ data class VideoInfo(
/** /**
* The mimetype of the video e.g. "video/mp4". * The mimetype of the video e.g. "video/mp4".
*/ */
@Json(name = "mimetype") val mimeType: String, @Json(name = "mimetype") val mimeType: String?,
/** /**
* The width of the video in pixels. * The width of the video in pixels.

View file

@ -16,11 +16,12 @@
package im.vector.matrix.android.api.session.room.send package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem
/** /**
* Tag class for spans that should mention a user. * Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send * These Spans will be transformed into pills when detected in message to send
*/ */
interface UserMentionSpan { interface UserMentionSpan {
val displayName: String val matrixItem: MatrixItem
val userId: String
} }

View file

@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
root.getClearContent().toModel<MessageStickerContent>() root.getClearContent().toModel<MessageStickerContent>()
} else { } else {
annotations?.editSummary?.aggregatedContent?.toModel() annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel() ?: root.getClearContent().toModel()
} }
} }
@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
if (lastMessageContent != null) { if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body ?: lastMessageContent.body
} }
return null return null

View file

@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.Cancelable
/** /**
* This interface defines a method to sign out. It's implemented at the session level. * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
*/ */
interface SignOutService { interface SignOutService {
/** /**
* Sign out * Ask the homeserver for a new access token.
* The same deviceId will be used
*/ */
fun signOut(callback: MatrixCallback<Unit>) fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable
/**
* Sign out, and release the session, clear all the session data, including crypto data
* @param sigOutFromHomeserver true if the sign out request has to be done
*/
fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -17,10 +17,11 @@
package im.vector.matrix.android.api.session.sync package im.vector.matrix.android.api.session.sync
sealed class SyncState { sealed class SyncState {
object IDLE : SyncState() object Idle : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState() data class Running(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState() object Paused : SyncState()
object KILLING : SyncState() object Killing : SyncState()
object KILLED : SyncState() object Killed : SyncState()
object NO_NETWORK : SyncState() object NoNetwork : SyncState()
object InvalidToken : SyncState()
} }

View file

@ -0,0 +1,141 @@
/*
* 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.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User
import java.util.*
sealed class MatrixItem(
open val id: String,
open val displayName: String?,
open val avatarUrl: String?
) {
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id
}
protected fun checkId() {
if (!id.startsWith(getIdPrefix())) {
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
}
}
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
is RoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
return getBestName()
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]
if (initial in listOf('@', '#', '+') && dn.length > 1) {
startIndex++
}
var length = 1
var first = dn[startIndex]
// LEFT-TO-RIGHT MARK
if (dn.length >= 2 && 0x200e == first.toInt()) {
startIndex++
first = dn[startIndex]
}
// check if its the start of a surrogate pair
if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
val second = dn[startIndex + 1]
if (second.toInt() in 0xDC00..0xDFFF) {
length++
}
}
dn.substring(startIndex, startIndex + length)
}
.toUpperCase(Locale.ROOT)
}
companion object {
private const val ircPattern = " (IRC)"
}
}
/* ==========================================================================================
* Extensions to create MatrixItem
* ========================================================================================== */
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)

View file

@ -53,7 +53,7 @@ internal abstract class AuthModule {
.name("matrix-sdk-auth.realm") .name("matrix-sdk-auth.realm")
.modules(AuthRealmModule()) .modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION) .schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration()) .migration(AuthRealmMigration)
.build() .build()
} }
} }

View file

@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
?.also { Timber.d("Overriding identity server url to $it") } ?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) } ?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri ?: homeServerConnectionConfig.identityServerUri
)) ),
isTokenValid = true)
sessionParamsStore.save(sessionParams) sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams) return sessionManager.getOrCreateSession(sessionParams)

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
internal interface SessionParamsStore { internal interface SessionParamsStore {
@ -28,6 +29,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams) suspend fun save(sessionParams: SessionParams)
suspend fun setTokenInvalid(userId: String)
suspend fun updateCredentials(newCredentials: Credentials)
suspend fun delete(userId: String) suspend fun delete(userId: String)
suspend fun deleteAll() suspend fun deleteAll()

View file

@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
internal class AuthRealmMigration : RealmMigration { internal object AuthRealmMigration : RealmMigration {
companion object { // Current schema version
// Current schema version const val SCHEMA_VERSION = 2L
const val SCHEMA_VERSION = 1L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
} }
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
} }
} }

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth.db package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.awaitTransaction
@ -75,6 +76,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
} }
} }
override suspend fun setTokenInvalid(userId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user $userId"
.let { Timber.w(it) }
.also { error(it) }
} else {
currentSessionParams.isTokenValid = false
}
}
}
override suspend fun updateCredentials(newCredentials: Credentials) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user ${newCredentials.userId}"
.let { Timber.w(it) }
.also { error(it) }
} else {
val newSessionParams = currentSessionParams.copy(
credentials = newCredentials,
isTokenValid = true
)
val entity = mapper.map(newSessionParams)
if (entity != null) {
realm.insertOrUpdate(entity)
}
}
}
}
override suspend fun delete(userId: String) { override suspend fun delete(userId: String) {
awaitTransaction(realmConfiguration) { awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java) it.where(SessionParamsEntity::class.java)

View file

@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity( internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "", @PrimaryKey var userId: String = "",
var credentialsJson: String = "", var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "" var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
) : RealmObject() ) : RealmObject()

View file

@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) { if (credentials == null || homeServerConnectionConfig == null) {
return null return null
} }
return SessionParams(credentials, homeServerConnectionConfig) return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
} }
fun map(sessionParams: SessionParams?): SessionParamsEntity? { fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentialsJson == null || homeServerConnectionConfigJson == null) { if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null return null
} }
return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson) return SessionParamsEntity(
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)
} }
} }

View file

@ -49,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type * @param mimetype the mime type
* @return the encryption file info * @return the encryption file info
*/ */
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult { fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom() val secureRandom = SecureRandom()

View file

@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.") Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) { when (failure.error.code) {
MatrixError.NOT_FOUND, MatrixError.M_NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> { MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version // Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure) backupAllGroupSessionsCallback?.onFailure(failure)

View file

@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor(
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState, versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList()
) )
} }
} }

View file

@ -39,7 +39,10 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var hasUnreadMessages: Boolean = false, var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null, var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
var flatAliases: String = ""
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View file

@ -32,6 +32,18 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
return query return query
} }
internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? {
val roomSummary = realm.where<RoomSummaryEntity>()
.equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias)
.findFirst()
if (roomSummary != null) {
return roomSummary
}
return realm.where<RoomSummaryEntity>()
.contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias")
.findFirst()
}
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst() ?: realm.createObject(roomId) return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
} }

View file

@ -16,19 +16,29 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import javax.inject.Inject import javax.inject.Inject
internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor { internal class AccessTokenInterceptor @Inject constructor(
@UserId private val userId: String,
private val sessionParamsStore: SessionParamsStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request() var request = chain.request()
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set accessToken?.let {
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken) val newRequestBuilder = request.newBuilder()
request = newRequestBuilder.build() // Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
request = newRequestBuilder.build()
}
return chain.proceed(request) return chain.proceed(request)
} }
private val accessToken
get() = sessionParamsStore.get(userId)?.credentials?.accessToken
} }

View file

@ -20,8 +20,8 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -32,6 +32,7 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -99,7 +100,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) { if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management // Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
} }
return Failure.ServerError(matrixError, httpCode) return Failure.ServerError(matrixError, httpCode)

View file

@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## decrypt file") Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
} else { ?: throw IllegalStateException("Decryption error")
inputStream
} }
writeToFile(inputStream, destFile)
destFile
} }
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else { } else {
Try.just(destFile) Try.just(destFile)
} }

View file

@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>, private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>) private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null private var syncThread: SyncThread? = null
override val isOpenable: Boolean
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
@MainThread @MainThread
override fun open() { override fun open() {
assertMainThread() assertMainThread()
@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { fun onGlobalError(globalError: GlobalError) {
sessionListeners.dispatchConsentNotGiven(consentNotGivenError) if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)
}
}
sessionListeners.dispatchGlobalError(globalError)
} }
override fun contentUrlResolver() = contentUrlResolver override fun contentUrlResolver() = contentUrlResolver

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import javax.inject.Inject import javax.inject.Inject
@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
} }
} }
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) { fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) { synchronized(listeners) {
listeners.forEach { listeners.forEach {
it.onConsentNotGivenError(consentNotGivenError) it.onGlobalError(globalError)
} }
} }
} }

View file

@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
suspend fun uploadFile(file: File, suspend fun uploadFile(file: File,
filename: String?, filename: String?,
mimeType: String, mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull()) val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }

View file

@ -25,11 +25,13 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
@ -45,6 +47,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {
@ -111,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable {
return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
@ -258,4 +259,12 @@ internal interface RoomAPI {
fun reportContent(@Path("roomId") roomId: String, fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Body body: ReportContentBody): Call<Unit> @Body body: ReportContentBody): Call<Unit>
/**
* Get the room ID associated to the room alias.
*
* @param roomAlias the room alias.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
} }

View file

@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.DefaultFileService
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask
@ -133,4 +135,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
@Binds
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
} }

View file

@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -68,7 +70,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
unreadNotifications: RoomSyncUnreadNotifications? = null, unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) { updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
if (roomSummary != null) { if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
@ -91,15 +93,24 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId) val otherRoomMembers = RoomMembers(realm, roomId)

View file

@ -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.session.room.alias
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
data class Params(
val roomAlias: String,
val searchOnServer: Boolean
)
}
internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monarchy: Monarchy,
private val roomAPI: RoomAPI) : GetRoomIdByAliasTask {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
}
return if (roomId != null) {
Optional.from(roomId)
} else if (!params.searchOnServer) {
Optional.from<String>(null)
} else {
roomId = executeRequest<RoomAliasDescription> {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}.roomId
Optional.from(roomId)
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.alias
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RoomAliasDescription(
/**
* The room ID for this alias.
*/
@Json(name = "room_id") val roomId: String,
/**
* A list of servers that are aware of this room ID.
*/
@Json(name = "servers") val servers: List<String> = emptyList()
)

View file

@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params) apiCall = roomAPI.createRoom(params)
} }
val roomId = createRoomResponse.roomId!! val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm -> val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java) realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomEntityFields.ROOM_ID, roomId)

View file

@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_AUDIO, type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
audioInfo = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg", mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_FILE, type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file", body = attachment.name ?: "file",
info = FileInfo( info = FileInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
?: "application/octet-stream", ?: "application/octet-stream",
size = attachment.size size = attachment.size
), ),

View file

@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean { private fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {

View file

@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // append the pill
append(String.format(template, urlSpan.userId, urlSpan.displayName)) append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
currIndex = end currIndex = end
} }
// append text after the last pill // append text after the last pill

View file

@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService { private val taskExecutor: TaskExecutor) : SignOutService {
override fun signOut(callback: MatrixCallback<Unit>) { override fun signInAgain(password: String,
signOutTask callback: MatrixCallback<Unit>): Cancelable {
.configureWith { return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask
.configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View file

@ -0,0 +1,56 @@
/*
* 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.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> {
data class Params(
val password: String
)
}
internal class DefaultSignInAgainTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val sessionParams: SessionParams,
private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
override suspend fun execute(params: SignInAgainTask.Params) {
val newCredentials = executeRequest<Credentials> {
apiCall = signOutAPI.loginAgain(
PasswordLoginParams.userIdentifier(
// Reuse the same userId
sessionParams.credentials.userId,
params.password,
// The spec says the initial device name will be ignored
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
// but https://github.com/matrix-org/synapse/issues/6525
// Reuse the same deviceId
deviceId = sessionParams.credentials.deviceId
)
)
}
sessionParamsStore.updateCredentials(newCredentials)
}
}

View file

@ -16,12 +16,27 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
internal interface SignOutAPI { internal interface SignOutAPI {
/**
* Attempt to login again to the same account.
* Set all the timeouts to 1 minute
* It is similar to [AuthAPI.login]
*
* @param loginParams the login parameters
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials>
/** /**
* Invalidate the access token, so that it can no longer be used for authorization. * Invalidate the access token, so that it can no longer be used for authorization.
*/ */

View file

@ -37,8 +37,11 @@ internal abstract class SignOutModule {
} }
@Binds @Binds
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
@Binds @Binds
abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
@Binds
abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
} }

View file

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
import android.content.Context import android.content.Context
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.crypto.CryptoModule
@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit> internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params(
val sigOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(private val context: Context, internal class DefaultSignOutTask @Inject constructor(private val context: Context,
@UserId private val userId: String, @UserId private val userId: String,
@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask { @UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) { override suspend fun execute(params: SignOutTask.Params) {
Timber.d("SignOut: send request...") // It should be done even after a soft logout, to be sure the deviceId is deleted on the
executeRequest<Unit> { if (params.sigOutFromHomeserver) {
apiCall = signOutAPI.signOut() Timber.d("SignOut: send request...")
try {
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
} catch (throwable: Throwable) {
// Maybe due to https://github.com/matrix-org/synapse/issues/5755
if (throwable is Failure.ServerError
&& throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also throwable.error.isSoftLogout should be true
// Ignore
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
} else {
throw throwable
}
}
} }
Timber.d("SignOut: release session...") Timber.d("SignOut: release session...")

View file

@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
} }
val syncResponse = try { val syncResponse = executeRequest<SyncResponse> {
executeRequest<SyncResponse> { apiCall = syncAPI.sync(requestParams)
apiCall = syncAPI.sync(requestParams)
}
} catch (throwable: Throwable) {
// Intercept 401
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete(userId)
}
throw throwable
} }
syncResponseHandler.handleResponse(syncResponse, token) syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch) syncTokenStore.saveToken(syncResponse.nextBatch)

View file

@ -147,7 +147,7 @@ open class SyncService : Service() {
} }
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token, stop the thread
stopSelf() stopSelf()
} }

View file

@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { ) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.IDLE private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>() private var liveState = MutableLiveData<SyncState>()
private val lock = Object() private val lock = Object()
private var cancelableTask: Cancelable? = null private var cancelableTask: Cancelable? = null
private var isStarted = false private var isStarted = false
private var isTokenValid = true
init { init {
updateStateTo(SyncState.IDLE) updateStateTo(SyncState.Idle)
} }
fun setInitialForeground(initialForeground: Boolean) { fun setInitialForeground(initialForeground: Boolean) {
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
updateStateTo(newState) updateStateTo(newState)
} }
@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (!isStarted) { if (!isStarted) {
Timber.v("Resume sync...") Timber.v("Resume sync...")
isStarted = true isStarted = true
// Check again the token validity
isTokenValid = true
lock.notify() lock.notify()
} }
} }
@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
fun kill() = synchronized(lock) { fun kill() = synchronized(lock) {
Timber.v("Kill sync...") Timber.v("Kill sync...")
updateStateTo(SyncState.KILLING) updateStateTo(SyncState.Killing)
cancelableTask?.cancel() cancelableTask?.cancel()
lock.notify() lock.notify()
} }
@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.register(this) networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this) backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) { while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state") Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) { if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...") Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK) updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else if (!isStarted) { } else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...") Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED) updateStateTo(SyncState.Paused)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isTokenValid) {
Timber.v("Token is invalid. Waiting...")
updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else { } else {
if (state !is SyncState.RUNNING) { if (state !is SyncState.Running) {
updateStateTo(SyncState.RUNNING(afterPause = true)) updateStateTo(SyncState.Running(afterPause = true))
} }
// No timeout after a pause // No timeout after a pause
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout") Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} else if (failure is Failure.Cancelled) { } else if (failure is Failure.Cancelled) {
Timber.v("Cancelled") Timber.v("Cancelled")
} else if (failure is Failure.ServerError } else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token
Timber.w(failure) Timber.w(failure)
updateStateTo(SyncState.KILLING) isTokenValid = false
isStarted = false
} else { } else {
Timber.e(failure) Timber.e(failure)
@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await() latch.await()
state.let { state.let {
if (it is SyncState.RUNNING && it.afterPause) { if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.RUNNING(afterPause = false)) updateStateTo(SyncState.Running(afterPause = false))
} }
} }
@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} }
} }
Timber.v("Sync killed") Timber.v("Sync killed")
updateStateTo(SyncState.KILLED) updateStateTo(SyncState.Killed)
backgroundDetectionObserver.unregister(this) backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this) networkConnectivityChecker.unregister(this)
} }

View file

@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.util package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber import timber.log.Timber
import java.util.Locale
/** /**
* Convert a string to an UTF8 String * Convert a string to an UTF8 String
@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
s s
} }
} }
fun String?.firstLetterOfDisplayName(): String {
if (this.isNullOrEmpty()) return ""
val isUserId = MatrixPatterns.isUserId(this)
val firstLetterIndex = if (isUserId) 1 else 0
return this[firstLetterIndex].toString().toUpperCase(Locale.ROOT)
}

View file

@ -82,4 +82,86 @@
<string name="room_displayname_empty_room">Prázdná místnost</string> <string name="room_displayname_empty_room">Prázdná místnost</string>
<string name="notice_room_update">%s upravil/a tuto místnost.</string>
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Zpráva smazána [smazal/a %1$s] [důvod: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">"%1$s obnovil/a pozvánku do místnosti pro %2$s"</string>
<string name="verification_emoji_cat">Kočka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kůň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prase</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Králík</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohout</string>
<string name="verification_emoji_penguin">Tučnák</string>
<string name="verification_emoji_turtle">Želva</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnice</string>
<string name="verification_emoji_butterfly">Motýl</string>
<string name="verification_emoji_flower">Květina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Houba</string>
<string name="verification_emoji_globe">Glóbus</string>
<string name="verification_emoji_moon">Měsíc</string>
<string name="verification_emoji_cloud">Mrak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukuřice</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Dort</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Smajlík</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobouk</string>
<string name="verification_emoji_glasses">Brýle</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
<string name="verification_emoji_umbrella">Deštník</string>
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Dárek</string>
<string name="verification_emoji_lightbulb">Žárovka</string>
<string name="verification_emoji_book">Knížka</string>
<string name="verification_emoji_pencil">Tužka</string>
<string name="verification_emoji_paperclip">Sponka</string>
<string name="verification_emoji_scissors">Nůžky</string>
<string name="verification_emoji_lock">Zámek</string>
<string name="verification_emoji_key">Klíč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Kolo</string>
<string name="verification_emoji_airplane">Letadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Pohár</string>
<string name="verification_emoji_ball">Míč</string>
<string name="verification_emoji_guitar">Kytara</string>
<string name="verification_emoji_trumpet">Trumpeta</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Sluchátka</string>
<string name="verification_emoji_folder">Složka</string>
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
\nStahuji účet…</string>
<string name="initial_sync_start_importing_account_crypto">Uvodní synchronizace:
\nStahuji klíče</string>
<string name="initial_sync_start_importing_account_rooms">Uvodní synchnizace:
\nStahuji místnost</string>
<string name="initial_sync_start_importing_account_joined_rooms">Uvodní synchronizace:
\nStahuji moje místnosti</string>
<string name="initial_sync_start_importing_account_left_rooms">Uvodní synchonizace:
\nStahuji místnosti, které jsem opustil/a</string>
<string name="initial_sync_start_importing_account_groups">Úvodní sychronizace:
\nImportuji komunity</string>
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
\nImportuji data účtu</string>
<string name="event_status_sending_message">Posílám zprávu…</string>
</resources> </resources>

View file

@ -49,7 +49,7 @@
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string> <string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string> <string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string> <string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">관련 대화</string> <string name="message_reply_to_prefix">관련 대화</string>

View file

@ -88,70 +88,70 @@
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string> <string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string> <string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string> <string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
<string name="verification_emoji_dog">Pes</string> <string name="verification_emoji_dog">Hlava psa</string>
<string name="verification_emoji_cat">Mačka</string> <string name="verification_emoji_cat">Hlava mačky</string>
<string name="verification_emoji_lion">Lev</string> <string name="verification_emoji_lion">Hlava leva</string>
<string name="verification_emoji_horse">Kôň</string> <string name="verification_emoji_horse">Kôň</string>
<string name="verification_emoji_unicorn">Jednorožec</string> <string name="verification_emoji_unicorn">Hlava jednorožca</string>
<string name="verification_emoji_pig">Prasa</string> <string name="verification_emoji_pig">Hlava prasaťa</string>
<string name="verification_emoji_elephant">Slon</string> <string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Zajac</string> <string name="verification_emoji_rabbit">Hlava zajaca</string>
<string name="verification_emoji_panda">Panda</string> <string name="verification_emoji_panda">Hlava pandy</string>
<string name="verification_emoji_rooster">Kohút</string> <string name="verification_emoji_rooster">Kohút</string>
<string name="verification_emoji_penguin">Tučniak</string> <string name="verification_emoji_penguin">Tučniak</string>
<string name="verification_emoji_turtle">Korytnačka</string> <string name="verification_emoji_turtle">Korytnačka</string>
<string name="verification_emoji_fish">Ryba</string> <string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnica</string> <string name="verification_emoji_octopus">Chobotnica</string>
<string name="verification_emoji_butterfly">Motýľ</string> <string name="verification_emoji_butterfly">Motýľ</string>
<string name="verification_emoji_flower">Kvetina</string> <string name="verification_emoji_flower">Tulipán</string>
<string name="verification_emoji_tree">Strom</string> <string name="verification_emoji_tree">Listnatý strom</string>
<string name="verification_emoji_cactus">Kaktus</string> <string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Hríb</string> <string name="verification_emoji_mushroom">Huba</string>
<string name="verification_emoji_globe">Zemeguľa</string> <string name="verification_emoji_globe">Zemeguľa</string>
<string name="verification_emoji_moon">Mesiac</string> <string name="verification_emoji_moon">Polmesiac</string>
<string name="verification_emoji_cloud">Oblak</string> <string name="verification_emoji_cloud">Oblak</string>
<string name="verification_emoji_fire">Oheň</string> <string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string> <string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string> <string name="verification_emoji_apple">Červené jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string> <string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukurica</string> <string name="verification_emoji_corn">Kukuričný klas</string>
<string name="verification_emoji_pizza">Pizza</string> <string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Koláč</string> <string name="verification_emoji_cake">Narodeninová torta</string>
<string name="verification_emoji_heart">Srdce</string> <string name="verification_emoji_heart">Červené</string>
<string name="verification_emoji_smiley">Úsmev</string> <string name="verification_emoji_smiley">Škeriaca sa tvár</string>
<string name="verification_emoji_robot">Robot</string> <string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobúk</string> <string name="verification_emoji_hat">Cylinder</string>
<string name="verification_emoji_glasses">Okuliare</string> <string name="verification_emoji_glasses">Okuliare</string>
<string name="verification_emoji_wrench">Skrutkovač</string> <string name="verification_emoji_wrench">Francúzsky kľúč</string>
<string name="verification_emoji_santa">Mikuláš</string> <string name="verification_emoji_santa">Santa Claus</string>
<string name="verification_emoji_thumbsup">Palec nahor</string> <string name="verification_emoji_thumbsup">Palec nahor</string>
<string name="verification_emoji_umbrella">Dáždnik</string> <string name="verification_emoji_umbrella">Dáždnik</string>
<string name="verification_emoji_hourglass">Presýpacie hodiny</string> <string name="verification_emoji_hourglass">Presýpacie hodiny</string>
<string name="verification_emoji_clock">Hodiny</string> <string name="verification_emoji_clock">Budík</string>
<string name="verification_emoji_gift">Darček</string> <string name="verification_emoji_gift">Zabalený darček</string>
<string name="verification_emoji_lightbulb">Žiarovka</string> <string name="verification_emoji_lightbulb">Žiarovka</string>
<string name="verification_emoji_book">Kniha</string> <string name="verification_emoji_book">Zatvorená kniha</string>
<string name="verification_emoji_pencil">Ceruzka</string> <string name="verification_emoji_pencil">Ceruzka</string>
<string name="verification_emoji_paperclip">Kancelárska sponka</string> <string name="verification_emoji_paperclip">Sponka na papier</string>
<string name="verification_emoji_scissors">Nožnice</string> <string name="verification_emoji_scissors">Nožnice</string>
<string name="verification_emoji_lock">Zámok</string> <string name="verification_emoji_lock">Zatvorená zámka</string>
<string name="verification_emoji_key">Kľúč</string> <string name="verification_emoji_key">Kľúč</string>
<string name="verification_emoji_hammer">Kladivo</string> <string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefón</string> <string name="verification_emoji_telephone">Telefón</string>
<string name="verification_emoji_flag">Vlajka</string> <string name="verification_emoji_flag">Kockovaná zástava</string>
<string name="verification_emoji_train">Vlak</string> <string name="verification_emoji_train">Rušeň</string>
<string name="verification_emoji_bicycle">Bicykel</string> <string name="verification_emoji_bicycle">Bicykel</string>
<string name="verification_emoji_airplane">Lietadlo</string> <string name="verification_emoji_airplane">Lietadlo</string>
<string name="verification_emoji_rocket">Raketa</string> <string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string> <string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Lopta</string> <string name="verification_emoji_ball">Futbal</string>
<string name="verification_emoji_guitar">Gitara</string> <string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Trúbka</string> <string name="verification_emoji_trumpet">Trúbka</string>
<string name="verification_emoji_bell">Zvonček</string> <string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string> <string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Schlúchadlá</string> <string name="verification_emoji_headphone">Slúchadlá</string>
<string name="verification_emoji_folder">Priečinok</string> <string name="verification_emoji_folder">Fascikel</string>
<string name="verification_emoji_pin">Pin</string> <string name="verification_emoji_pin">Špendlík</string>
<string name="initial_sync_start_importing_account">Úvodná synchronizácia: <string name="initial_sync_start_importing_account">Úvodná synchronizácia:
\nPrebieha import účtu…</string> \nPrebieha import účtu…</string>
@ -173,4 +173,5 @@
<string name="event_status_sending_message">Odosielanie správy…</string> <string name="event_status_sending_message">Odosielanie správy…</string>
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string> <string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>
<string name="notice_room_third_party_revoked_invite">%1$s zamietol pozvanie používateľa %2$s vstúpiť do miestnosti</string>
</resources> </resources>

View file

@ -13,7 +13,7 @@
<string name="notice_room_ban">%1$s 封禁了 %2$s</string> <string name="notice_room_ban">%1$s 封禁了 %2$s</string>
<string name="notice_avatar_url_changed">%1$s 更换了他们的头像</string> <string name="notice_avatar_url_changed">%1$s 更换了他们的头像</string>
<string name="notice_display_name_set">%1$s 将他们的昵称设置为 %2$s</string> <string name="notice_display_name_set">%1$s 将他们的昵称设置为 %2$s</string>
<string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string> <string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string>
<string name="notice_display_name_removed">%1$s 移除了他们的昵称 (%2$s)</string> <string name="notice_display_name_removed">%1$s 移除了他们的昵称 (%2$s)</string>
<string name="notice_room_topic_changed">%1$s 把主题改为: %2$s</string> <string name="notice_room_topic_changed">%1$s 把主题改为: %2$s</string>
<string name="notice_room_name_changed">%1$s 把聊天室名称改为: %2$s</string> <string name="notice_room_name_changed">%1$s 把聊天室名称改为: %2$s</string>
@ -167,4 +167,7 @@
<string name="event_status_sending_message">正在发送消息…</string> <string name="event_status_sending_message">正在发送消息…</string>
<string name="clear_timeline_send_queue">清除正在发送队列</string> <string name="clear_timeline_send_queue">清除正在发送队列</string>
<string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</string>
<string name="verification_emoji_pin">置顶</string>
</resources> </resources>

View file

@ -243,4 +243,18 @@
<string name="event_status_sending_message">Sending message…</string> <string name="event_status_sending_message">Sending message…</string>
<string name="clear_timeline_send_queue">Clear sending queue</string> <string name="clear_timeline_send_queue">Clear sending queue</string>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
</resources> </resources>

View file

@ -2,19 +2,6 @@
<resources> <resources>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
<string name="no_network_indicator">There is no network connection right now</string>
</resources> </resources>

View file

@ -1 +1 @@
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx' include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'

View file

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 10 ext.versionMinor = 11
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -229,6 +229,7 @@ dependencies {
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch")
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -292,6 +293,7 @@ dependencies {
implementation 'me.gujun.android:span:1.7' implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version" implementation "io.noties.markwon:html:$markwon_version"
implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.4'
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1' implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version" implementation "androidx.autofill:autofill:$autofill_version"
@ -341,8 +343,6 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
} }
implementation 'diff_match_patch:diff_match_patch:current'
implementation "androidx.emoji:emoji-appcompat:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0"
// TESTS // TESTS

View file

@ -31,4 +31,8 @@
<issue id="ViewConstructor" severity="error" /> <issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" /> <issue id="UseValueOf" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar"/>
</issue>
</lint> </lint>

View file

@ -65,7 +65,13 @@
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" /> <activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" /> <activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" /> <activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" /> <activity
android:name=".features.home.room.detail.RoomDetailActivity"
android:parentActivityName=".features.home.HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".features.home.HomeActivity" />
</activity>
<activity android:name=".features.debug.DebugMenuActivity" /> <activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" /> <activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" /> <activity android:name=".features.webview.VectorWebViewActivity" />
@ -98,6 +104,22 @@
<category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.OPENABLE" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".features.signout.hard.SignedOutActivity" />
<activity
android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.permalink.PermalinkHandlerActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
</activity>
<!-- Services --> <!-- Services -->
<service <service

View file

@ -359,6 +359,11 @@ SOFTWARE.
<br/> <br/>
Copyright 2018 Kumar Bibek Copyright 2018 Kumar Bibek
</li> </li>
<li>
<b>htmlcompressor</b>
<br/>
Copyright 2017 Sergiy Kovalchuk
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View file

@ -37,7 +37,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
activeSession.set(session) activeSession.set(session)
sessionObservableStore.post(Option.fromNullable(session)) sessionObservableStore.post(Option.just(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session) incomingVerificationRequestHandler.start(session)
} }

View file

@ -47,6 +47,7 @@ import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFr
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@Module @Module
interface FragmentModule { interface FragmentModule {
@ -261,4 +262,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(EmojiChooserFragment::class) @FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SoftLogoutFragment::class)
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
} }

View file

@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -40,6 +41,7 @@ import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.permalink.PermalinkHandlerActivity
import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
@ -49,6 +51,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
@Component( @Component(
@ -78,6 +81,8 @@ interface ScreenComponent {
fun navigator(): Navigator fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity) fun inject(activity: HomeActivity)
@ -126,6 +131,10 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View file

@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication import im.vector.riotx.VectorApplication
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
@ -37,6 +38,7 @@ import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.group.SelectedGroupDataSource import im.vector.riotx.features.home.group.SelectedGroupDataSource
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.* import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
@ -86,8 +88,12 @@ interface VectorComponent {
fun eventHtmlRenderer(): EventHtmlRenderer fun eventHtmlRenderer(): EventHtmlRenderer
fun vectorHtmlCompressor(): VectorHtmlCompressor
fun navigator(): Navigator fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun homeRoomListObservableStore(): HomeRoomListDataSource fun homeRoomListObservableStore(): HomeRoomListDataSource
fun shareRoomListObservableStore(): ShareRoomListDataSource fun shareRoomListObservableStore(): ShareRoomListDataSource

View file

@ -26,6 +26,8 @@ import dagger.Provides
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.error.DefaultErrorFormatter
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
@ -72,6 +74,9 @@ abstract class VectorModule {
@Binds @Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter
@Binds @Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
} }

View file

@ -14,25 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.rx package im.vector.riotx.core.epoxy
import im.vector.matrix.android.api.MatrixCallback import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.Cancelable import im.vector.riotx.R
import io.reactivex.SingleEmitter
internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> { /**
* Item of size (0, 0).
* It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes.
*/
@EpoxyModelClass(layout = R.layout.item_zero)
abstract class ZeroItem : VectorEpoxyModel<ZeroItem.Holder>() {
override fun onSuccess(data: T) { class Holder : VectorEpoxyHolder()
singleEmitter.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
singleEmitter.tryOnError(failure)
}
}
fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
singleEmitter.setCancellable {
this.cancel()
}
} }

View file

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -37,11 +38,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarUrl: String lateinit var matrixItem: MatrixItem
@EpoxyAttribute
lateinit var senderId: String
@EpoxyAttribute
var senderName: String? = null
@EpoxyAttribute @EpoxyAttribute
lateinit var body: CharSequence lateinit var body: CharSequence
@EpoxyAttribute @EpoxyAttribute
@ -50,8 +47,8 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
var movementMethod: MovementMethod? = null var movementMethod: MovementMethod? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, senderId, senderName, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
holder.sender.setTextOrHide(senderName) holder.sender.setTextOrHide(matrixItem.displayName)
holder.body.movementMethod = movementMethod holder.body.movementMethod = movementMethod
holder.body.text = body holder.body.text = body
body.findPillsAndProcess { it.bind(holder.body) } body.findPillsAndProcess { it.bind(holder.body) }

View file

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -36,16 +37,12 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarUrl: String lateinit var matrixItem: MatrixItem
@EpoxyAttribute
lateinit var roomId: String
@EpoxyAttribute
var roomName: String? = null
@EpoxyAttribute var settingsClickListener: View.OnClickListener? = null @EpoxyAttribute var settingsClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, roomId, roomName, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
holder.roomName.setTextOrHide(roomName) holder.roomName.setTextOrHide(matrixItem.displayName)
holder.roomSettings.setOnClickListener(settingsClickListener) holder.roomSettings.setOnClickListener(settingsClickListener)
} }

View file

@ -25,14 +25,15 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { interface ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String
}
fun toHumanReadable(failure: Failure): String { class DefaultErrorFormatter @Inject constructor(
// Default private val stringProvider: StringProvider
return failure.localizedMessage ) : ErrorFormatter {
}
fun toHumanReadable(throwable: Throwable?): String { override fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) { return when (throwable) {
null -> null null -> null
is Failure.NetworkConnection -> { is Failure.NetworkConnection -> {
@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout) stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException -> throwable.ioException is UnknownHostException ->
// Invalid homeserver? // Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)
else -> else ->
stringProvider.getString(R.string.error_no_network) stringProvider.getString(R.string.error_no_network)
@ -52,23 +54,23 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
// Special case for terms and conditions // Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted) stringProvider.getString(R.string.error_terms_not_accepted)
} }
throwable.error.code == MatrixError.FORBIDDEN throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> { && throwable.error.message == "Invalid password" -> {
stringProvider.getString(R.string.auth_invalid_login_param) stringProvider.getString(R.string.auth_invalid_login_param)
} }
throwable.error.code == MatrixError.USER_IN_USE -> { throwable.error.code == MatrixError.M_USER_IN_USE -> {
stringProvider.getString(R.string.login_signup_error_user_in_use) stringProvider.getString(R.string.login_signup_error_user_in_use)
} }
throwable.error.code == MatrixError.BAD_JSON -> { throwable.error.code == MatrixError.M_BAD_JSON -> {
stringProvider.getString(R.string.login_error_bad_json) stringProvider.getString(R.string.login_error_bad_json)
} }
throwable.error.code == MatrixError.NOT_JSON -> { throwable.error.code == MatrixError.M_NOT_JSON -> {
stringProvider.getString(R.string.login_error_not_json) stringProvider.getString(R.string.login_error_not_json)
} }
throwable.error.code == MatrixError.LIMIT_EXCEEDED -> { throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
limitExceededError(throwable.error) limitExceededError(throwable.error)
} }
throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> { throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found) stringProvider.getString(R.string.login_reset_password_error_not_found)
} }
else -> { else -> {

View file

@ -21,6 +21,6 @@ import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean { fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& this.error.code == MatrixError.UNAUTHORIZED) && error.code == MatrixError.M_UNAUTHORIZED)
} }

View file

@ -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.core.error
import im.vector.riotx.BuildConfig
import timber.log.Timber
/**
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
*/
fun fatalError(message: String) {
if (BuildConfig.DEBUG) {
error(message)
} else {
Timber.e(message)
}
}

View file

@ -19,6 +19,7 @@ package im.vector.riotx.core.extensions
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
@ -40,3 +41,11 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler // @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// @Inject lateinit var keyRequestHandler: KeyRequestHandler // @Inject lateinit var keyRequestHandler: KeyRequestHandler
} }
/**
* Tell is the session has unsaved e2e keys in the backup
*/
fun Session.hasUnsavedKeys(): Boolean {
return inboundGroupSessionsCount(false) > 0
&& getKeysBackupService().state != KeysBackupState.ReadyToBackUp
}

View file

@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder
return this return this
} }
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
fun String?.toReducedUrl(): String {
return (this ?: "")
.substringAfter("://")
.trim { it == '/' }
}

View file

@ -38,12 +38,15 @@ import butterknife.Unbinder
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.* import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.consent.ConsentNotGivenHelper import im.vector.riotx.features.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var activeSessionHolder: ActiveSessionHolder
// Filter for multiple invalid token error
private var mainActivityStarted = false
private var unBinder: Unbinder? = null private var unBinder: Unbinder? = null
private var savedInstanceState: Bundle? = null private var savedInstanceState: Bundle? = null
@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}) })
sessionListener = getVectorComponent().sessionListener() sessionListener = getVectorComponent().sessionListener()
sessionListener.consentNotGivenLiveData.observeEvent(this) { sessionListener.globalErrorLiveData.observeEvent(this) {
consentNotGivenHelper.displayDialog(it.consentUri, handleGlobalError(it)
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
} }
doBeforeSetContentView() doBeforeSetContentView()
@ -180,6 +185,33 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
} }
} }
private fun handleGlobalError(globalError: GlobalError) {
when (globalError) {
is GlobalError.InvalidToken ->
handleInvalidToken(globalError)
is GlobalError.ConsentNotGivenError ->
consentNotGivenHelper.displayDialog(globalError.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
}
}
protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
Timber.w("Invalid token event received")
if (mainActivityStarted) {
return
}
mainActivityStarted = true
MainActivity.restartApp(this,
MainActivityArgs(
clearCredentials = !globalError.softLogout,
isUserLoggedOut = true,
isSoftLogout = globalError.softLogout
)
)
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unBinder?.unbind() unBinder?.unbind()
@ -190,8 +222,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.i("onResume Activity ${this.javaClass.simpleName}")
Timber.v("onResume Activity ${this.javaClass.simpleName}")
configurationViewModel.onActivityResumed() configurationViewModel.onActivityResumed()

View file

@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import timber.log.Timber
/** /**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -80,6 +81,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${this.javaClass.simpleName}")
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply { return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog val dialog = this as? BottomSheetDialog

View file

@ -34,6 +34,7 @@ import com.bumptech.glide.util.Util.assertMainThread
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -49,12 +50,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
} }
/* ========================================================================================== /* ==========================================================================================
* Navigator * Navigator and other common objects
* ========================================================================================== */ * ========================================================================================== */
protected lateinit var navigator: Navigator
private lateinit var screenComponent: ScreenComponent private lateinit var screenComponent: ScreenComponent
protected lateinit var navigator: Navigator
protected lateinit var errorFormatter: ErrorFormatter
/* ========================================================================================== /* ==========================================================================================
* View model * View model
* ========================================================================================== */ * ========================================================================================== */
@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator() navigator = screenComponent.navigator()
errorFormatter = screenComponent.errorFormatter()
viewModelFactory = screenComponent.viewModelFactory() viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector()) injectWith(injector())
@ -100,7 +104,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
@CallSuper @CallSuper
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.v("onResume Fragment ${this.javaClass.simpleName}") Timber.i("onResume Fragment ${this.javaClass.simpleName}")
} }
@CallSuper @CallSuper

View file

@ -23,6 +23,8 @@ import android.widget.ProgressBar
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -59,9 +61,9 @@ open class UserAvatarPreference : Preference {
val session = mSession ?: return val session = mSession ?: return
val view = mAvatarView ?: return val view = mAvatarView ?: return
session.getUser(session.myUserId)?.let { session.getUser(session.myUserId)?.let {
avatarRenderer.render(it, view) avatarRenderer.render(it.toMatrixItem(), view)
} ?: run { } ?: run {
avatarRenderer.render(null, session.myUserId, null, view) avatarRenderer.render(MatrixItem.UserItem(session.myUserId), view)
} }
} }

View file

@ -14,9 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.api.failure package im.vector.riotx.core.ui.model
// This data class will be sent to the bus // android.util.Size in API 21+
data class ConsentNotGivenError( data class Size(val width: Int, val height: Int)
val consentUri: String
)

View file

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem
import kotlinx.android.synthetic.main.view_read_receipts.view.* import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5 private const val MAX_RECEIPT_DISPLAYED = 5
@ -59,7 +60,7 @@ class ReadReceiptsView @JvmOverloads constructor(
receiptAvatars[index].visibility = View.INVISIBLE receiptAvatars[index].visibility = View.INVISIBLE
} else { } else {
receiptAvatars[index].visibility = View.VISIBLE receiptAvatars[index].visibility = View.VISIBLE
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index])
} }
} }

View file

@ -19,9 +19,11 @@ package im.vector.riotx.features
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.signout.hard.SignedOutActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Parcelize
data class MainActivityArgs(
val clearCache: Boolean = false,
val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false,
val isSoftLogout: Boolean = false
) : Parcelable
/**
* This is the entry point of RiotX
* This Activity, when started with argument, is also doing some cleanup when user disconnects,
* clears cache, is logged out, or is soft logged out
*/
class MainActivity : VectorBaseActivity() { class MainActivity : VectorBaseActivity() {
companion object { companion object {
private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE" private const val EXTRA_ARGS = "EXTRA_ARGS"
private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS"
// Special action to clear cache and/or clear credentials // Special action to clear cache and/or clear credentials
fun restartApp(activity: Activity, clearCache: Boolean = false, clearCredentials: Boolean = false) { fun restartApp(activity: Activity, args: MainActivityArgs) {
val intent = Intent(activity, MainActivity::class.java) val intent = Intent(activity, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra(EXTRA_CLEAR_CACHE, clearCache) intent.putExtra(EXTRA_ARGS, args)
intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials)
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
private lateinit var args: MainActivityArgs
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false) args = parseArgs()
val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false)
if (args.clearCredentials || args.isUserLoggedOut) {
clearNotifications()
}
// Handle some wanted cleanup // Handle some wanted cleanup
if (clearCache || clearCredentials) { if (args.clearCache || args.clearCredentials) {
doCleanUp(clearCache, clearCredentials) doCleanUp()
} else { } else {
start() startNextActivityAndFinish()
} }
} }
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) { private fun clearNotifications() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()
notificationDrawerManager.persistInfo()
}
private fun parseArgs(): MainActivityArgs {
val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS)
Timber.w("Starting MainActivity with $argsFromIntent")
return MainActivityArgs(
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
private fun doCleanUp() {
when { when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> { args.clearCredentials -> sessionHolder.getActiveSession().signOut(
override fun onSuccess(data: Unit) { !args.isUserLoggedOut,
Timber.w("SIGN_OUT: success, start app") object : MatrixCallback<Unit> {
sessionHolder.clearActiveSession() override fun onSuccess(data: Unit) {
doLocalCleanupAndStart() Timber.w("SIGN_OUT: success, start app")
} sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials) displayError(failure)
} }
}) })
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> { args.clearCache -> sessionHolder.getActiveSession().clearCache(
override fun onSuccess(data: Unit) { object : MatrixCallback<Unit> {
doLocalCleanupAndStart() override fun onSuccess(data: Unit) {
} doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials) displayError(failure)
} }
}) })
} }
} }
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanupAndStart() { private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
// On UI Thread // On UI Thread
@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() {
} }
} }
start() startNextActivityAndFinish()
} }
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) { private fun displayError(failure: Throwable) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure)) .setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) } .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setNegativeButton(R.string.cancel) { _, _ -> start() } .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
.setCancelable(false) .setCancelable(false)
.show() .show()
} }
private fun start() { private fun startNextActivityAndFinish() {
val intent = if (sessionHolder.hasActiveSession()) { val intent = when {
HomeActivity.newIntent(this) args.clearCredentials
} else { && !args.isUserLoggedOut ->
LoginActivity.newIntent(this, null) // User has explicitly asked to log out
LoginActivity.newIntent(this, null)
args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this)
args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reason
SignedOutActivity.newIntent(this)
sessionHolder.hasActiveSession() ->
// We have a session.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this)
} else {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
}
else ->
// First start, or no active session
LoginActivity.newIntent(this, null)
} }
startActivity(intent) startActivity(intent)
finish() finish()

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.* import com.kbeanie.multipicker.api.entity.*
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import timber.log.Timber
fun ChosenContact.toContactAttachment(): ContactAttachment { fun ChosenContact.toContactAttachment(): ContactAttachment {
return ContactAttachment( return ContactAttachment(
@ -29,6 +30,7 @@ fun ChosenContact.toContactAttachment(): ContactAttachment {
} }
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData( return ContentAttachmentData(
path = originalPath, path = originalPath,
mimeType = mimeType, mimeType = mimeType,
@ -40,6 +42,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
} }
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData( return ContentAttachmentData(
path = originalPath, path = originalPath,
mimeType = mimeType, mimeType = mimeType,
@ -51,16 +54,17 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
) )
} }
fun ChosenFile.mapType(): ContentAttachmentData.Type { private fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when { return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE else -> ContentAttachmentData.Type.FILE
} }
} }
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData( return ContentAttachmentData(
path = originalPath, path = originalPath,
mimeType = mimeType, mimeType = mimeType,
@ -75,6 +79,7 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
} }
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData( return ContentAttachmentData(
path = originalPath, path = originalPath,
mimeType = mimeType, mimeType = mimeType,

View file

@ -18,11 +18,12 @@ package im.vector.riotx.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class AutocompleteUserController @Inject constructor(): TypedEpoxyController<List<User>>() { class AutocompleteUserController @Inject constructor() : TypedEpoxyController<List<User>>() {
var listener: AutocompleteClickListener<User>? = null var listener: AutocompleteClickListener<User>? = null
@ -35,9 +36,7 @@ class AutocompleteUserController @Inject constructor(): TypedEpoxyController<Lis
data.forEach { user -> data.forEach { user ->
autocompleteUserItem { autocompleteUserItem {
id(user.userId) id(user.userId)
userId(user.userId) matrixItem(user.toMatrixItem())
name(user.displayName)
avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
clickListener { _ -> clickListener { _ ->
listener?.onItemClick(user) listener?.onItemClick(user)

View file

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -30,15 +31,13 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() { abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var name: String? = null @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
holder.nameView.text = name holder.nameView.text = matrixItem.getBestName()
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View file

@ -22,6 +22,8 @@ import androidx.lifecycle.Observer
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -57,10 +59,10 @@ class SASVerificationIncomingFragment @Inject constructor(
otherDeviceTextView.text = viewModel.otherDeviceId otherDeviceTextView.text = viewModel.otherDeviceId
viewModel.otherUser?.let { viewModel.otherUser?.let {
avatarRenderer.render(it, avatarImageView) avatarRenderer.render(it.toMatrixItem(), avatarImageView)
} ?: run { } ?: run {
// Fallback to what we know // Fallback to what we know
avatarRenderer.render(null, viewModel.otherUserId ?: "", viewModel.otherUserId, avatarImageView) avatarRenderer.render(MatrixItem.UserItem(viewModel.otherUserId ?: "", viewModel.otherUserId), avatarImageView)
} }
viewModel.transactionState.observe(viewLifecycleOwner, Observer { viewModel.transactionState.observe(viewLifecycleOwner, Observer {

View file

@ -27,10 +27,7 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.glide.GlideRequest
@ -45,76 +42,42 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
companion object { companion object {
private const val THUMBNAIL_SIZE = 250 private const val THUMBNAIL_SIZE = 250
private val AVATAR_COLOR_LIST = listOf(
R.color.riotx_avatar_fill_1,
R.color.riotx_avatar_fill_2,
R.color.riotx_avatar_fill_3
)
} }
@UiThread @UiThread
fun render(roomSummary: RoomSummary, imageView: ImageView) { fun render(matrixItem: MatrixItem, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) render(imageView.context,
} GlideApp.with(imageView),
matrixItem,
@UiThread DrawableImageViewTarget(imageView))
fun render(user: User, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView))
}
@UiThread
fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))
} }
@UiThread @UiThread
fun render(context: Context, fun render(context: Context,
glideRequest: GlideRequests, glideRequest: GlideRequests,
avatarUrl: String?, matrixItem: MatrixItem,
identifier: String,
name: String?,
target: Target<Drawable>) { target: Target<Drawable>) {
val displayName = if (name.isNullOrBlank()) { val placeholder = getPlaceholderDrawable(context, matrixItem)
identifier buildGlideRequest(glideRequest, matrixItem.avatarUrl)
} else {
name
}
val placeholder = getPlaceholderDrawable(context, identifier, displayName)
buildGlideRequest(glideRequest, avatarUrl)
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
} }
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable { fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
val avatarColor = ContextCompat.getColor(context, getColorFromUserId(identifier)) val avatarColor = when (matrixItem) {
return if (text.isEmpty()) { is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id))
TextDrawable.builder().buildRound("", avatarColor) else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id))
} else {
val firstLetter = text.firstLetterOfDisplayName()
TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(firstLetter, avatarColor)
} }
return TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
} }
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
// private fun getAvatarColor(text: String? = null): Int {
// var colorIndex: Long = 0
// if (!text.isNullOrEmpty()) {
// var sum: Long = 0
// for (i in 0 until text.length) {
// sum += text[i].toLong()
// }
// colorIndex = sum % AVATAR_COLOR_LIST.size
// }
// return AVATAR_COLOR_LIST[colorIndex.toInt()]
// }
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)

View file

@ -27,6 +27,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
@ -74,12 +75,7 @@ class HomeDetailFragment @Inject constructor(
private fun onGroupChange(groupSummary: GroupSummary?) { private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let { groupSummary?.let {
avatarRenderer.render( avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView)
it.avatarUrl,
it.groupId,
it.displayName,
groupToolbarAvatarImageView
)
} }
} }
@ -155,7 +151,7 @@ class HomeDetailFragment @Inject constructor(
bottomNavigationView.selectedItemId = when (displayMode) { bottomNavigationView.selectedItemId = when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home else -> R.id.bottom_action_home
} }
} }

Some files were not shown because too many files have changed in this diff Show more