Merge branch 'develop' into feature/read_marker

This commit is contained in:
ganfra 2019-09-19 16:39:39 +02:00
commit ea0809ff87
145 changed files with 4083 additions and 1447 deletions

View File

@ -1,6 +1,7 @@
# Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk
# Last docker plugin version can be found here:
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean)
# Build debug version of the RiotX application, from the develop branch and the features branches
@ -18,6 +19,7 @@ steps:
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Assemble FDroid Debug version"
agents:
@ -32,6 +34,7 @@ steps:
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Build Google Play unsigned APK"
agents:
@ -46,6 +49,7 @@ steps:
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
# Code quality

View File

@ -1,27 +1,50 @@
Changes in RiotX 0.5.0 (2019-XX-XX)
Changes in RiotX 0.6.0 (2019-XX-XX)
===================================================
Features:
-
- Save draft of a message when exiting a room with non empty composer (#329)
Improvements:
- Reduce default release build log level, and lab option to enable more logs.
- Add unread indent on room list (#485)
- Message Editing: Update notifications (#128)
- Remove any notification of a redacted event (#563)
Other changes:
-
Bugfix:
- Fix crash due to missing informationData (#535)
- Progress in initial sync dialog is decreasing for a step and should not (#532)
- Fix characters erased from the Search field when the result are coming (#545)
- "No connection" banner was displayed by mistake
Translations:
-
Build:
- Fix issue with version name (#533)
- Fix rendering issue of accepted third party invitation event
- Fix (again) issue with bad versionCode generated by Buildkite (#553)
Changes in RiotX 0.4.0 (2019-XX-XX)
Changes in RiotX 0.5.0 (2019-09-17)
===================================================
Features:
- Implementation of login to homeserver with SSO
- Handle M_CONSENT_NOT_GIVEN error (#64)
- Auto configure homeserver and identity server URLs of LoginActivity with a magic link
Improvements:
- Reduce default release build log level, and lab option to enable more logs.
- Display a no network indicator when there is no network (#559)
Bugfix:
- Fix crash due to missing informationData (#535)
- Progress in initial sync dialog is decreasing for a step and should not (#532)
- Fix rendering issue of accepted third party invitation event
- All current notifications were dismissed by mistake when the app is launched from the launcher
Build:
- Fix issue with version name (#533)
- Fix issue with bad versionCode generated by Buildkite (#553)
Changes in RiotX 0.4.0 (2019-08-30)
===================================================
Features:

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
import io.reactivex.Observable
@ -63,6 +64,10 @@ class RxRoom(private val room: Room) {
return room.getEventReadReceiptsLive(eventId).asObservable()
}
fun liveDrafts(): Observable<List<UserDraft>> {
return room.getDraftsLive().asObservable()
}
}
fun Room.rx(): RxRoom {

View File

@ -139,6 +139,9 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
// Bus
implementation 'org.greenrobot:eventbus:3.1.1'
debugImplementation 'com.airbnb.okreplay:okreplay:1.4.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.4.0'
androidTestImplementation 'com.airbnb.okreplay:espresso:1.4.0'

View File

@ -17,16 +17,23 @@
package im.vector.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
/**
* This interface defines methods to authenticate to a matrix server.
*/
interface Authenticator {
/**
* Request the supported login flows for this homeserver
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
/**
* @param homeServerConnectionConfig this param is used to configure the Homeserver
* @param login the login field
@ -56,4 +63,9 @@ interface Authenticator {
* @return the associated session if any, or null
*/
fun getSession(sessionParams: SessionParams): Session?
/**
* Create a session after a SSO successful login
*/
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.failure
// This data class will be sent to the bus
data class ConsentNotGivenError(
val consentUri: String
)

View File

@ -31,6 +31,7 @@ import java.io.IOException
*/
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
// When server send an error, but it cannot be interpreted as a MatrixError

View File

@ -43,6 +43,7 @@ interface PushRuleService {
interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>)
fun onRoomLeft(roomId: String)
fun onEventRedacted(redactedEventId: String)
fun batchFinish()
}
}

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
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.securestorage.SecureStorageService
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.SyncState
@ -50,7 +51,8 @@ interface Session :
FileService,
PushRuleService,
PushersService,
InitialSyncProgressService {
InitialSyncProgressService,
SecureStorageService {
/**
* The params associated to the session
@ -87,7 +89,7 @@ interface Session :
/**
* This method start the sync thread.
*/
fun startSync(fromForeground : Boolean)
fun startSync(fromForeground: Boolean)
/**
* This method stop the sync thread.

View File

@ -109,8 +109,6 @@ interface CryptoService {
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
fun clearCryptoCache(callback: MatrixCallback<Unit>)
fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
@ -32,6 +33,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
interface Room :
TimelineService,
SendService,
DraftService,
ReadService,
MembershipService,
StateService,

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
/**
@ -29,14 +30,16 @@ data class RoomSummary(
val topic: String = "",
val avatarUrl: String = "",
val isDirect: Boolean = false,
val latestEvent: TimelineEvent? = null,
val latestPreviewableEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(),
val notificationCount: Int = 0,
val highlightCount: Int = 0,
val hasUnreadMessages: Boolean = false,
val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null
val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList()
) {
val isVersioned: Boolean

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.send
import androidx.lifecycle.LiveData
interface DraftService {
/**
* Save or update a draft to the room
*/
fun saveDraft(draft: UserDraft)
/**
* Delete the last draft, basically just after sending the message
*/
fun deleteDraft()
/**
* Return the current drafts if any, as a live data
* The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
*/
fun getDraftsLive(): LiveData<List<UserDraft>>
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.send
/**
* Describes a user draft:
* REGULAR: draft of a classical message
* QUOTE: draft of a message which quotes another message
* EDIT: draft of an edition of a message
* REPLY: draft of a reply of another message
*/
sealed class UserDraft(open val text: String) {
data class REGULAR(override val text: String) : UserDraft(text)
data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text)
data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text)
data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text)
fun isValid(): Boolean {
return when (this) {
is REGULAR -> text.isNotBlank()
else -> true
}
}
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
@ -93,6 +94,13 @@ data class TimelineEvent(
*/
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
/**
* Get the eventId which was edited by this event if any
*/
fun TimelineEvent.getEditedEventId(): String? {
return root.getClearContent().toModel<MessageContent>()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId
}
/**
* Get last MessageContent, after a possible edition
*/
@ -100,6 +108,20 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSu
?: root.getClearContent().toModel()
/**
* Get last Message body, after a possible edition
*/
fun TimelineEvent.getLastMessageBody(): String? {
val lastMessageContent = getLastMessageContent()
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body ?: lastMessageContent.body
}
return null
}
fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.securestorage
import java.io.InputStream
import java.io.OutputStream
interface SecureStorageService {
fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream)
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T?
}

View File

@ -22,4 +22,5 @@ sealed class SyncState {
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
}

View File

@ -17,10 +17,12 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
@ -29,6 +31,13 @@ import retrofit2.http.POST
*/
internal interface AuthAPI {
/**
* Get the supported login flow
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun getLoginFlows(): Call<LoginFlowResponse>
/**
* Pass params to the server for the current login phase.
* Set all the timeouts to 1 minute

View File

@ -23,7 +23,7 @@ import dagger.Provides
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.RealmConfiguration
import java.io.File
@ -33,16 +33,21 @@ internal abstract class AuthModule {
@Module
companion object {
private const val DB_ALIAS = "matrix-sdk-auth"
@JvmStatic
@Provides
@AuthDatabase
fun providesRealmConfiguration(context: Context): RealmConfiguration {
fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
val old = File(context.filesDir, "matrix-sdk-auth")
if (old.exists()) {
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
}
return RealmConfiguration.Builder()
.configureEncryption("matrix-sdk-auth", context)
.apply {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
.deleteRealmIfMigrationNeeded()

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.di.Unauthenticated
@ -62,11 +63,20 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(homeServerConnectionConfig, login, password)
@ -74,7 +84,14 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
}
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
@ -95,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
sessionManager.getOrCreateSession(sessionParams)
}
override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)

View File

@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow(
@Json(name = "stages")
val stages: List<String>? = null
)
) {
companion object {
// Possible values for type
const val TYPE_LOGIN_SSO = "m.login.sso"
const val TYPE_LOGIN_TOKEN = "m.login.token"
const val TYPE_LOGIN_PASSWORD = "m.login.password"
}
}

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class LoginFlowResponse(
data class LoginFlowResponse(
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
)

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -30,12 +29,13 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import retrofit2.Retrofit
import java.io.File
@ -45,17 +45,20 @@ internal abstract class CryptoModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "crypto_module_"
@JvmStatic
@Provides
@CryptoDatabase
@SessionScope
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
val userIDHash = credentials.userId.md5()
fun providesRealmConfiguration(@UserCacheDirectory directory: File,
@UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(File(context.filesDir, userIDHash))
.configureEncryption("crypto_module_$userIDHash", context)
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)

View File

@ -62,11 +62,9 @@ import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
@ -135,7 +133,6 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor
@ -1047,14 +1044,6 @@ internal class DefaultCryptoService @Inject constructor(
}
}
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
clearCryptoDataTask
.configureWith {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
roomDecryptorProvider.addNewSessionListener(newSessionListener)
}

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import javax.inject.Inject
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,
private val olmDevice: MXOlmDevice) {
private val olmDevice: MXOlmDevice) {
/**
* Sign Object

View File

@ -27,14 +27,14 @@ import java.util.*
import javax.inject.Inject
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
private val cryptoStore: IMXCryptoStore,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
/**
* Try to make sure we have established olm sessions for the given users.
* @param users a list of user ids.
*/
suspend fun handle(users: List<String>) : MXUsersDevicesMap<MXOlmSessionResult> {
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()

View File

@ -23,8 +23,8 @@ import timber.log.Timber
import javax.inject.Inject
internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore,
private val credentials: Credentials,
private val keysBackup: KeysBackup) {
private val credentials: Credentials,
private val keysBackup: KeysBackup) {
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
val device = cryptoStore.getUserDevice(deviceId, userId)

View File

@ -28,14 +28,14 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor(private val credentials: Credentials,
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
fun create(): MXMegolmDecryption {
return MXMegolmDecryption(

View File

@ -17,10 +17,12 @@ package im.vector.matrix.android.internal.database
import android.content.Context
import android.util.Base64
import im.vector.matrix.android.api.util.SecretStoringUtils
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
import io.realm.RealmConfiguration
import timber.log.Timber
import java.security.SecureRandom
import javax.inject.Inject
/**
* On creation a random key is generated, this key is then encrypted using the system KeyStore.
@ -34,12 +36,13 @@ import java.security.SecureRandom
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
*/
private object RealmKeysUtils {
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
internal class RealmKeysUtils @Inject constructor(context: Context,
private val secretStoringUtils: SecretStoringUtils) {
private val rng = SecureRandom()
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
private fun generateKeyForRealm(): ByteArray {
val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH)
rng.nextBytes(keyForRealm)
@ -49,8 +52,7 @@ private object RealmKeysUtils {
/**
* Check if there is already a key for this alias
*/
fun hasKeyForDatabase(alias: String, context: Context): Boolean {
val sharedPreferences = getSharedPreferences(context)
private fun hasKeyForDatabase(alias: String): Boolean {
return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias")
}
@ -59,13 +61,12 @@ private object RealmKeysUtils {
* The random key is then encrypted by the keystore, and the encrypted key is stored
* in shared preferences.
*
* @return the generate key (can be passed to Realm Configuration)
* @return the generated key (can be passed to Realm Configuration)
*/
fun createAndSaveKeyForDatabase(alias: String, context: Context): ByteArray {
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
val key = generateKeyForRealm()
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context)
val sharedPreferences = getSharedPreferences(context)
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
sharedPreferences
.edit()
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
@ -77,30 +78,43 @@ private object RealmKeysUtils {
* Retrieves the key for this database
* throws if something goes wrong
*/
fun extractKeyForDatabase(alias: String, context: Context): ByteArray {
val sharedPreferences = getSharedPreferences(context)
private fun extractKeyForDatabase(alias: String): ByteArray {
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context)
val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
return Base64.decode(b64!!, Base64.NO_PADDING)
}
private fun getSharedPreferences(context: Context) =
context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
}
fun RealmConfiguration.Builder.configureEncryption(alias: String, context: Context): RealmConfiguration.Builder {
if (RealmKeysUtils.hasKeyForDatabase(alias, context)) {
Timber.i("Found key for alias:$alias")
RealmKeysUtils.extractKeyForDatabase(alias, context).also {
this.encryptionKey(it)
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
val key = if (hasKeyForDatabase(alias)) {
Timber.i("Found key for alias:$alias")
extractKeyForDatabase(alias)
} else {
Timber.i("Create key for DB alias:$alias")
createAndSaveKeyForDatabase(alias)
}
} else {
Timber.i("Create key for DB alias:$alias")
RealmKeysUtils.createAndSaveKeyForDatabase(alias, context).also {
this.encryptionKey(it)
if (BuildConfig.LOG_PRIVATE_DATA) {
val log = key.joinToString("") { "%02x".format(it) }
Timber.w("Database key for alias `$alias`: $log")
}
realmConfigurationBuilder.encryptionKey(key)
}
// Delete elements related to the alias
fun clear(alias: String) {
if (hasKeyForDatabase(alias)) {
secretStoringUtils.safeDeleteKey(alias)
sharedPreferences
.edit()
.remove("${ENCRYPTED_KEY_PREFIX}_$alias")
.apply()
}
}
return this
companion object {
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.database.mapper
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.model.DraftEntity
/**
* DraftEntity <-> UserDraft
*/
internal object DraftMapper {
fun map(entity: DraftEntity): UserDraft {
return when (entity.draftMode) {
DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content)
DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content)
DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content)
DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content)
else -> null
} ?: UserDraft.REGULAR("")
}
fun map(domain: UserDraft): DraftEntity {
return when (domain) {
is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
}
}
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.UUID
import java.util.*
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(
@ -35,7 +35,7 @@ internal class RoomSummaryMapper @Inject constructor(
RoomTag(it.tagName, it.tagOrder)
}
val latestEvent = roomSummaryEntity.latestEvent?.let {
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
timelineEventMapper.map(it)
}
if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
@ -53,20 +53,23 @@ internal class RoomSummaryMapper @Inject constructor(
}
}
return RoomSummary(
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",
topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect,
latestEvent = latestEvent,
latestPreviewableEvent = latestEvent,
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount,
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
tags = tags,
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList()
)
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
internal open class DraftEntity(var content: String = "",
var draftMode: String = MODE_REGULAR,
var linkedEventId: String = ""
) : RealmObject() {
companion object {
const val MODE_REGULAR = "REGULAR"
const val MODE_EDIT = "EDIT"
const val MODE_REPLY = "REPLY"
const val MODE_QUOTE = "QUOTE"
}
}

View File

@ -26,7 +26,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var avatarUrl: String? = "",
var topic: String? = "",
var latestEvent: TimelineEventEntity? = null,
var latestPreviewableEvent: TimelineEventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
@ -35,8 +35,10 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var readMarkerId: String? = null
var userDrafts: UserDraftsEntity? = null
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name

View File

@ -44,6 +44,8 @@ import io.realm.annotations.RealmModule
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class
])
internal class SessionRealmModule

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
/**
* Create a specific table to be able to do direct query on it and keep the draft ordered
*/
internal open class UserDraftsEntity(var userDrafts: RealmList<DraftEntity> = RealmList()
) : RealmObject() {
// Link to RoomSummaryEntity
@LinkingObjects("userDrafts")
val roomSummaryEntity: RealmResults<RoomSummaryEntity>? = null
companion object
}

View File

@ -0,0 +1,42 @@
/*
* 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.database.query
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
internal fun isEventRead(monarchy: Monarchy,
userId: String?,
roomId: String?,
eventId: String?): Boolean {
if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
return false
}
var isEventRead = false
monarchy.doWithRealm { realm ->
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex ?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead
}

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.database.query
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.model.UserDraftsEntity
import im.vector.matrix.android.internal.database.model.UserDraftsEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<UserDraftsEntity> {
val query = realm.where<UserDraftsEntity>()
if (roomId != null) {
query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId)
}
return query
}

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.internal.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UserCacheDirectory

View File

@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.di
import javax.inject.Scope
/**
* Use the annotation @MatrixScope to annotate classes we want the SDK to instantiate only once
*/
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)

View File

@ -35,7 +35,7 @@ internal object NetworkModule {
@Provides
@JvmStatic
fun providesHttpLogingInterceptor(): HttpLoggingInterceptor {
fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logger = FormattedJsonHttpLogger()
val interceptor = HttpLoggingInterceptor(logger)
interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL

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.internal.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UserMd5

View File

@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -35,29 +34,38 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
.withDisconnectableCallbacks()
.build(context)
private val merlinsBeard = MerlinsBeard.Builder().build(context)
private val listeners = Collections.synchronizedList(ArrayList<Listener>())
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
// True when internet is available
var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected
private set
init {
merlin.bind()
merlin.registerDisconnectable {
Timber.v("On Disconnect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
if (hasInternetAccess) {
Timber.v("On Disconnect")
hasInternetAccess = false
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
}
}
}
merlin.registerConnectable {
Timber.v("On Connect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
if (!hasInternetAccess) {
Timber.v("On Connect")
hasInternetAccess = true
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
}
}
}
}
suspend fun waitUntilConnected() {
if (isConnected()) {
if (hasInternetAccess) {
return
} else {
suspendCoroutine<Unit> { continuation ->
@ -79,10 +87,6 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
listeners.remove(listener)
}
fun isConnected(): Boolean {
return merlinsBeard.isConnected
}
interface Listener {
fun onConnect() {

View File

@ -18,10 +18,13 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.CancellationException
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import timber.log.Timber
import java.io.IOException
@ -47,6 +50,7 @@ internal class Request<DATA> {
is IOException -> Failure.NetworkConnection(exception)
is Failure.ServerError,
is Failure.OtherServerError -> exception
is CancellationException -> Failure.Cancelled(exception)
else -> Failure.Unknown(exception)
}
}
@ -65,6 +69,11 @@ internal class Request<DATA> {
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
}
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {

View File

@ -24,7 +24,7 @@ import timber.log.Timber
import javax.inject.Inject
@MatrixScope
internal class UserAgentHolder @Inject constructor(val context: Context) {
internal class UserAgentHolder @Inject constructor(private val context: Context) {
var userAgent: String = ""
private set

View File

@ -35,16 +35,15 @@ import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
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.securestorage.SecureStorageService
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.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.util.MatrixCallbackDelegate
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
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.SyncWorker
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@ -65,6 +64,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val pushersService: Lazy<PushersService>,
private val cryptoService: Lazy<DefaultCryptoService>,
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val contentUploadProgressTracker: ContentUploadStateTracker,
@ -75,13 +75,13 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
CacheService by cacheService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get() {
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get() {
private var isOpen = false
@ -144,43 +144,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
}
@MainThread
override fun signOut(callback: MatrixCallback<Unit>) {
Timber.w("SIGN_OUT: start")
assert(isOpen)
Timber.w("SIGN_OUT: call webservice")
return signOutService.get().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache")
stopSync()
stopAnyBackgroundSync()
// Clear the cache
cacheService.get().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache")
cryptoService.get().clearCryptoCache(MatrixCallbackDelegate(callback))
WorkManagerUtil.cancelAllWorks(context)
callback.onSuccess(Unit)
}
override fun onFailure(failure: Throwable) {
// ignore error
Timber.e("SIGN_OUT: clear cache -> ERROR: ignoring")
onSuccess(Unit)
}
})
}
override fun onFailure(failure: Throwable) {
// Ignore failure
Timber.e("SIGN_OUT: call webservice -> ERROR: ignoring")
onSuccess(Unit)
}
})
}
override fun clearCache(callback: MatrixCallback<Unit>) {
stopSync()
stopAnyBackgroundSync()

View File

@ -27,19 +27,20 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.*
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
@ -51,6 +52,7 @@ internal abstract class SessionModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "session_db_"
@JvmStatic
@Provides
@ -65,18 +67,33 @@ internal abstract class SessionModule {
return sessionParams.credentials
}
@JvmStatic
@UserMd5
@Provides
fun providesUserMd5(sessionParams: SessionParams): String {
return sessionParams.credentials.userId.md5()
}
@JvmStatic
@Provides
@UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File {
return File(context.filesDir, userMd5)
}
@JvmStatic
@Provides
@SessionDatabase
@SessionScope
fun providesRealmConfiguration(sessionParams: SessionParams, context: Context): RealmConfiguration {
val childPath = sessionParams.credentials.userId.md5()
val directory = File(context.filesDir, childPath)
fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory directory: File,
@UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.name("disk_store.realm")
.configureEncryption("session_db_$childPath", context)
.apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
.build()
@ -99,7 +116,18 @@ internal abstract class SessionModule {
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient {
return okHttpClient.newBuilder()
.addInterceptor(accessTokenInterceptor)
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(accessTokenInterceptor)
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
}
@ -140,4 +168,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
}

View File

@ -132,6 +132,16 @@ internal class DefaultPushRuleService @Inject constructor(
}
}
fun dispatchRedactedEventId(redactedEventId: String) {
try {
listeners.forEach {
it.onEventRedacted(redactedEventId)
}
} catch (e: Throwable) {
Timber.e(e, "Error while dispatching room left")
}
}
fun dispatchFinish() {
try {
listeners.forEach {

View File

@ -78,6 +78,25 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
defaultPushRuleService.dispatchBing(event, it)
}
}
val allRedactedEvents = params.syncResponse.join
.map { entries ->
entries.value.timeline?.events?.filter {
it.type == EventType.REDACTION
}
.orEmpty()
.mapNotNull { it.redacts }
}
.fold(emptyList<String>(), { acc, next ->
acc + next
})
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
allRedactedEvents.forEach { redactedEventId ->
defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
}
defaultPushRuleService.dispatchFinish()
}

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
@ -40,6 +41,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val roomSummaryMapper: RoomSummaryMapper,
private val timelineService: TimelineService,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
private val readService: ReadService,
private val cryptoService: CryptoService,
@ -48,6 +50,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
) : Room,
TimelineService by timelineService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
ReadService by readService,
RelationService by relationService,

View File

@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
@ -38,6 +39,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val cryptoService: CryptoService,
private val timelineServiceFactory: DefaultTimelineService.Factory,
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory,
@ -51,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
roomSummaryMapper,
timelineServiceFactory.create(roomId),
sendServiceFactory.create(roomId),
draftServiceFactory.create(roomId),
stateServiceFactory.create(roomId),
readServiceFactory.create(roomId),
cryptoService,

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@ -26,6 +27,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
@ -39,7 +41,8 @@ import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(private val credentials: Credentials,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver) {
private val roomAvatarResolver: RoomAvatarResolver,
private val monarchy: Monarchy) {
// TODO: maybe allow user of SDK to give that list
private val PREVIEWABLE_TYPES = listOf(
@ -63,8 +66,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
membership: Membership? = null,
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) {
@ -85,9 +87,13 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.membership = membership
}
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
//avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, credentials.userId, roomId, latestPreviewableEvent?.eventId)
val otherRoomMembers = RoomMembers(realm, roomId)
.queryRoomMembersEvent()
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
@ -98,9 +104,8 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestEvent = latestEvent
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.draft
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.DraftMapper
import im.vector.matrix.android.internal.database.model.DraftEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.UserDraftsEntity
import im.vector.matrix.android.internal.database.query.where
import io.realm.kotlin.createObject
import timber.log.Timber
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy
) : DraftService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): DraftService
}
/**
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
* or even move an existing draft to the top of the list
*/
override fun saveDraft(draft: UserDraft) {
Timber.d("Draft: saveDraft ${privacySafe(draft)}")
monarchy.writeAsync { realm ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
val userDraftsEntity = roomSummaryEntity.userDrafts
?: realm.createObject<UserDraftsEntity>().also {
roomSummaryEntity.userDrafts = it
}
userDraftsEntity.let { userDraftEntity ->
// Save only valid draft
if (draft.isValid()) {
// Add a new draft or update the current one?
val newDraft = DraftMapper.map(draft)
// Is it an update of the top draft?
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
// top draft is an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
if (topDraft.linkedEventId == newDraft.linkedEventId) {
// Update the top draft
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
topDraft.content = newDraft.content
} else {
// Check a previously EDIT draft with the same id
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
}
if (existingEditDraftOfSameEvent != null) {
// Ignore the new text, restore what was typed before, by putting the draft to the top
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
} else {
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
}
} else {
// Add a new regular draft to the top
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
} else {
// Top draft is not an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else {
// Update the top draft
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
topDraft.draftMode = newDraft.draftMode
topDraft.content = newDraft.content
topDraft.linkedEventId = newDraft.linkedEventId
}
}
} else {
// There is no draft to save, so the composer was clear
Timber.d("Draft: delete a draft")
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: nothing to do")
} else {
// Remove the top draft
Timber.d("Draft: remove the top draft")
userDraftEntity.userDrafts.remove(topDraft)
}
}
}
}
}
private fun privacySafe(o: Any): Any {
if (BuildConfig.LOG_PRIVATE_DATA) {
return o
}
return ""
}
override fun deleteDraft() {
Timber.d("Draft: deleteDraft()")
monarchy.writeAsync { realm ->
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
if (userDraftsEntity.userDrafts.isNotEmpty()) {
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
}
}
}
}
override fun getDraftsLive(): LiveData<List<UserDraft>> {
val liveData = RealmLiveData(monarchy.realmConfiguration) {
UserDraftsEntity.where(it, roomId)
}
return Transformations.map(liveData) { userDraftsEntities ->
userDraftsEntities.firstOrNull()?.let { userDraftEntity ->
userDraftEntity.userDrafts.map { draftEntity ->
DraftMapper.map(draftEntity)
}
} ?: emptyList()
}
}
}

View File

@ -28,12 +28,10 @@ import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -80,19 +78,7 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
override fun isEventRead(eventId: String): Boolean {
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead
return isEventRead(monarchy, credentials.userId, roomId, eventId)
}
override fun getReadMarkerLive(): LiveData<Optional<String>> {

View File

@ -116,6 +116,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
?: return@awaitTransaction
roomSummary.notificationCount = 0
roomSummary.highlightCount = 0
roomSummary.hasUnreadMessages = false
}
}
}

View File

@ -17,33 +17,29 @@
package im.vector.matrix.android.internal.session.room.send
import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import androidx.work.WorkManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.work.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.events.model.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.DraftMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.content.UploadContentWorker
@ -75,6 +71,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
}
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
saveLocalEcho(it)
@ -165,12 +162,10 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
override fun deleteFailedEcho(localEcho: TimelineEvent) {
monarchy.writeAsync { realm ->
TimelineEventEntity.where(realm, eventId = localEcho.root.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
it.deleteFromRealm()
}
EventEntity.where(realm, eventId = localEcho.root.eventId
?: "").findFirst()?.let {
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
it.deleteFromRealm()
}
}

View File

@ -32,8 +32,8 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
}
internal class DefaultGetContextOfEventTask @Inject constructor(private val roomAPI: RoomAPI,
private val filterRepository: FilterRepository,
private val tokenChunkEventPersistor: TokenChunkEventPersistor
private val filterRepository: FilterRepository,
private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : GetContextOfEventTask {
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {

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.securestorage
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService {
override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) {
secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream)
}
override fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
return secretStoringUtils.loadSecureSecret(inputStream, keyAlias)
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.util
package im.vector.matrix.android.internal.session.securestorage
import android.content.Context
import android.os.Build
@ -22,17 +22,20 @@ import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import timber.log.Timber
import java.io.*
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.SecureRandom
import java.util.Calendar
import java.util.*
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
import javax.security.auth.x500.X500Principal
@ -65,22 +68,24 @@ import javax.security.auth.x500.X500Principal
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
* </code>
*
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
* You can also just use this utility to store a secret key, and use any encryption algorithm that you want.
*
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
* add a pin or change the schema); So you might and with a useless pile of bytes.
*/
object SecretStoringUtils {
internal class SecretStoringUtils @Inject constructor(private val context: Context) {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding";
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding"
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
const val FORMAT_API_M: Byte = 0
const val FORMAT_1: Byte = 1
const val FORMAT_2: Byte = 2
private const val FORMAT_API_M: Byte = 0
private const val FORMAT_1: Byte = 1
private const val FORMAT_2: Byte = 2
}
val keyStore: KeyStore by lazy {
private val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null)
}
@ -88,24 +93,30 @@ object SecretStoringUtils {
private val secureRandom = SecureRandom()
fun safeDeleteKey(keyAlias: String) {
try {
keyStore.deleteEntry(keyAlias)
} catch (e: KeyStoreException) {
Timber.e(e)
}
}
/**
* Encrypt the given secret using the android Keystore.
* On android >= M, will directly use the keystore to generate a symetric key
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
* On android >= M, will directly use the keystore to generate a symmetric key
* On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated
* in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned
* in the bytearray (in can be stored anywhere, it is encrypted)
* On older version a key in generated from alias with random salt.
*
* The secret is encrypted using the following method: AES/GCM/NoPadding
*/
@Throws(Exception::class)
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return encryptStringM(secret, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return encryptStringJ(secret, keyAlias, context)
} else {
return encryptForOldDevicesNotGood(secret, keyAlias)
fun securelyStoreString(secret: String, keyAlias: String): ByteArray? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias)
else -> encryptForOldDevicesNotGood(secret, keyAlias)
}
}
@ -113,39 +124,33 @@ object SecretStoringUtils {
* Decrypt a secret that was encrypted by #securelyStoreString()
*/
@Throws(Exception::class)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return decryptStringM(encrypted, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return decryptStringJ(encrypted, keyAlias, context)
} else {
return decryptForOldDevicesNotGood(encrypted, keyAlias)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias)
else -> decryptForOldDevicesNotGood(encrypted, keyAlias)
}
}
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
saveSecureObjectM(keyAlias, output, any)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return saveSecureObjectK(keyAlias, output, any, context)
} else {
return saveSecureObjectOldNotGood(keyAlias, output, any)
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any)
else -> saveSecureObjectOldNotGood(keyAlias, output, any)
}
}
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return loadSecureObjectM(keyAlias, inputStream)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return loadSecureObjectK(keyAlias, inputStream, context)
} else {
return loadSecureObjectOldNotGood(keyAlias, inputStream)
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream)
else -> loadSecureObjectOldNotGood(keyAlias, inputStream)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
if (secretKeyEntry == null) {
@ -163,7 +168,6 @@ object SecretStoringUtils {
return secretKeyEntry
}
/*
Symetric Key Generation is only available in M, so before M the idea is to:
- Generate a pair of RSA keys;
@ -172,8 +176,8 @@ object SecretStoringUtils {
- Store the encrypted AES
Generate a key pair for encryption
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
if (privateKeyEntry != null) return privateKeyEntry
@ -201,7 +205,7 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.M)
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
@ -212,10 +216,10 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.M)
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
@ -224,15 +228,15 @@ object SecretStoringUtils {
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun encryptStringK(text: String, keyAlias: String): ByteArray? {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val encryptedKey = rsaEncrypt(keyAlias, key)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
@ -242,7 +246,7 @@ object SecretStoringUtils {
return format1Make(encryptedKey, iv, encryptedBytes)
}
fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -258,11 +262,11 @@ object SecretStoringUtils {
return format2Make(salt, iv, encryptedBytes)
}
fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")
@ -277,25 +281,23 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
private fun decryptStringK(data: ByteArray, keyAlias: String): String? {
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
@ -314,14 +316,14 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val encryptedKey = rsaEncrypt(keyAlias, key)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
@ -342,7 +344,7 @@ object SecretStoringUtils {
output.write(bos1.toByteArray())
}
fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -387,8 +389,8 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val format = inputStream.read()
assert(format.toByte() == FORMAT_API_M)
@ -411,12 +413,12 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(IOException::class)
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
private fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? {
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
@ -432,8 +434,7 @@ object SecretStoringUtils {
}
@Throws(Exception::class)
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
private fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
val (salt, iv, encrypted) = format2Extract(inputStream)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -456,10 +457,10 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(Exception::class)
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
// Encrypt the text
val inputCipher = Cipher.getInstance(RSA_MODE)
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
@ -472,10 +473,10 @@ object SecretStoringUtils {
return outputStream.toByteArray()
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(Exception::class)
private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
val output = Cipher.getInstance(RSA_MODE)
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
@ -504,7 +505,6 @@ object SecretStoringUtils {
}
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_1)
@ -548,7 +548,6 @@ object SecretStoringUtils {
}
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_2)

View File

@ -16,25 +16,64 @@
package im.vector.matrix.android.internal.session.signout
import android.content.Context
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit>
internal class DefaultSignOutTask @Inject constructor(private val credentials: Credentials,
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
private val credentials: Credentials,
private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore) : SignOutTask {
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@UserCacheDirectory private val userFile: File,
private val realmKeysUtils: RealmKeysUtils,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
Timber.d("SignOut: send request...")
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
sessionParamsStore.delete(credentials.userId)
Timber.d("SignOut: release session...")
sessionManager.releaseSession(credentials.userId)
Timber.d("SignOut: cancel pending works...")
WorkManagerUtil.cancelAllWorks(context)
Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(credentials.userId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system")
userFile.deleteRecursively()
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
}
}

View File

@ -123,12 +123,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync,
isInitalSync: Boolean): RoomEntity {
isInitialSync: Boolean): RoomEntity {
Timber.v("Handle join sync for room $roomId")
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync)
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
}
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {

View File

@ -93,8 +93,8 @@ open class SyncService : Service() {
}
fun doSync(once: Boolean = false) {
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No internet access. Waiting...")
//TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {

View File

@ -50,6 +50,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val lock = Object()
private var cancelableTask: Cancelable? = null
private var isStarted = false
init {
updateStateTo(SyncState.IDLE)
}
@ -60,19 +62,18 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
if (!isStarted) {
Timber.v("Resume sync...")
updateStateTo(SyncState.RUNNING(afterPause = true))
isStarted = true
lock.notify()
}
}
fun pause() = synchronized(lock) {
if (state is SyncState.RUNNING) {
if (isStarted) {
Timber.v("Pause sync...")
updateStateTo(SyncState.PAUSED)
isStarted = false
cancelableTask?.cancel()
lock.notify()
}
}
@ -87,19 +88,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
return liveState
}
override fun onConnect() {
Timber.v("Network is back")
synchronized(lock) {
lock.notify()
}
}
override fun run() {
Timber.v("Start syncing...")
isStarted = true
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("No network or sync is Paused. Waiting...")
synchronized(lock) {
lock.wait()
}
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else {
if (state !is SyncState.RUNNING) {
@ -167,16 +180,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state to $newState")
Timber.v("Update state from $state to $newState")
state = newState
liveState.postValue(newState)
}
override fun onConnect() {
synchronized(lock) {
lock.notify()
}
}
override fun onMoveToForeground() {
restart()

View File

@ -33,7 +33,7 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
private set
private
val listeners = ArrayList<Listener>()
val listeners = LinkedHashSet<Listener>()
fun register(listener: Listener) {
listeners.add(listener)

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.worker
import android.content.Context
import androidx.work.*
// TODO Multiaccount
internal object WorkManagerUtil {
private const val MATRIX_SDK_TAG = "MatrixSDK"

View File

@ -3,4 +3,7 @@
<string name="no_network_indicator">There is no network connection right now</string>
</resources>

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
adb shell am start -a android.intent.action.VIEW -d "https://riot.im/config/config?hs_url=https%3A%2F%2Fmozilla-test.modular.im"

View File

@ -15,7 +15,7 @@ androidExtensions {
}
ext.versionMajor = 0
ext.versionMinor = 5
ext.versionMinor = 6
ext.versionPatch = 0
static def getGitTimestamp() {
@ -51,8 +51,15 @@ static def gitRevisionDate() {
}
static def gitBranchName() {
def cmd = "git rev-parse --abbrev-ref HEAD"
return cmd.execute().text.trim()
def fromEnv = System.env.BUILDKITE_BRANCH as String ?: ""
if (!fromEnv.isEmpty()) {
return fromEnv
} else {
// Note: this command return "HEAD" on Buildkite, so use the system env 'BUILDKITE_BRANCH' content first
def cmd = "git rev-parse --abbrev-ref HEAD"
return cmd.execute().text.trim()
}
}
static def getVersionSuffix() {
@ -75,7 +82,7 @@ project.android.buildTypes.all { buildType ->
// 64 bits have greater value than 32 bits
ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 }
def buildNumber = System.getenv("BUILDKITE_BUILD_NUMBER") as Integer ?: 0
def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android {
compileSdkVersion 28
@ -245,8 +252,9 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
// RXBinding
implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2'
implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
@ -273,6 +281,9 @@ dependencies {
implementation "ru.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
// Bus
implementation 'org.greenrobot:eventbus:3.1.1'
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.5'

View File

@ -196,13 +196,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// This ID can and should be used to detect duplicate notification requests.
val eventId = data["event_id"] ?: return //Just ignore
val eventType = data["type"]
if (eventType == null) {
//Just add a generic unknown event
val simpleNotifiableEvent = SimpleNotifiableEvent(
session.myUserId,
eventId,
null,
true, //It's an issue in this case, all event will bing even if expected to be silent.
title = getString(R.string.notification_unknown_new_event),
description = "",
@ -213,10 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
return
} else {
val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
@ -227,8 +224,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.e("--> ${event}")
}
} else {
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"]
@ -245,7 +240,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {

View File

@ -65,6 +65,19 @@
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" />
<activity android:name=".features.link.LinkHandlerActivity">
<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="https" />
<data android:host="riot.im" />
<data android:pathPrefix="/config/" />
</intent-filter>
</activity>
<!-- Services -->

View File

@ -349,6 +349,11 @@ SOFTWARE.
<br/>
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
</li>
<li>
<b>EventBus</b>
<br/>
Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)
</li>
</ul>

View File

@ -48,7 +48,6 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider
@ -73,6 +72,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
@ -112,7 +112,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
emojiCompatWrapper.init(fontRequest)
NotificationUtils.createNotificationChannels(applicationContext)
notificationUtils.createNotificationChannels()
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)

View File

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModelProvider
import dagger.BindsInstance
import dagger.Component
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyFragment
@ -42,15 +41,14 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.login.LoginFragment
import im.vector.riotx.features.login.LoginSsoFallbackFragment
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.navigation.Navigator
@ -65,20 +63,14 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.push.PushGatewaysFragment
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ScreenScope
interface ScreenComponent {
fun session(): Session
fun activeSessionHolder(): ActiveSessionHolder
fun viewModelFactory(): ViewModelProvider.Factory
@ -134,6 +126,10 @@ interface ScreenComponent {
fun inject(publicRoomsFragment: PublicRoomsFragment)
fun inject(loginFragment: LoginFragment)
fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment)
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
fun inject(quickReactionFragment: QuickReactionFragment)
@ -142,6 +138,8 @@ interface ScreenComponent {
fun inject(loginActivity: LoginActivity)
fun inject(linkHandlerActivity: LinkHandlerActivity)
fun inject(mainActivity: MainActivity)
fun inject(roomDirectoryActivity: RoomDirectoryActivity)

View File

@ -36,10 +36,7 @@ import im.vector.riotx.features.home.HomeRoomListObservableStore
import im.vector.riotx.features.home.group.SelectedGroupStore
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.NotifiableEventResolver
import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
@ -58,6 +55,8 @@ interface VectorComponent {
fun currentSession(): Session
fun notificationUtils(): NotificationUtils
fun notificationDrawerManager(): NotificationDrawerManager
fun appContext(): Context
@ -72,7 +71,7 @@ interface VectorComponent {
fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper() : EmojiCompatWrapper
fun emojiCompatWrapper(): EmojiCompatWrapper
fun eventHtmlRenderer(): EventHtmlRenderer

View File

@ -26,9 +26,9 @@ private const val KEY_DIALOG_IS_DISPLAYED = "DialogLocker.KEY_DIALOG_IS_DISPLAYE
/**
* Class to avoid displaying twice the same dialog
*/
class DialogLocker() : Restorable {
class DialogLocker(savedInstanceState: Bundle?) : Restorable {
private var isDialogDisplayed: Boolean = false
private var isDialogDisplayed = savedInstanceState?.getBoolean(KEY_DIALOG_IS_DISPLAYED, false) == true
private fun unlock() {
isDialogDisplayed = false

View File

@ -17,11 +17,13 @@
package im.vector.riotx.core.error
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import java.net.SocketTimeoutException
import javax.inject.Inject
class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
fun toHumanReadable(failure: Failure): String {
@ -32,10 +34,21 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) {
null -> null
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
is Failure.NetworkConnection -> {
if (throwable.ioException is SocketTimeoutException) {
stringProvider.getString(R.string.error_network_timeout)
} else {
stringProvider.getString(R.string.error_no_network)
}
}
is Failure.ServerError -> {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }
if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) {
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
} else {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }
}
}
else -> throwable.localizedMessage
}

View File

@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
@CallSuper
override fun injectWith(injector: ScreenComponent) {
session = injector.session()
session = injector.activeSessionHolder().getActiveSession()
}
override fun initUiAndData() {

View File

@ -36,11 +36,14 @@ import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxActivity
import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter
@ -50,6 +53,9 @@ import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.receivers.DebugReceiver
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
import kotlin.system.measureTimeMillis
@ -73,6 +79,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake
protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder
private var unBinder: Unbinder? = null
@ -127,6 +134,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
bugReporter = screenComponent.bugReporter()
rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator()
activeSessionHolder = screenComponent.activeSessionHolder()
configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed
@ -175,7 +183,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
configurationViewModel.onActivityResumed()
if (this !is BugReportActivity) {
rageShake?.start()
rageShake.start()
}
DebugReceiver
@ -190,7 +198,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
override fun onPause() {
super.onPause()
rageShake?.stop()
rageShake.stop()
debugReceiver?.let {
unregisterReceiver(debugReceiver)
@ -265,18 +273,21 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
return super.onOptionsItemSelected(item)
}
protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
// if (fm.backStackEntryCount == 0)
// return false
override fun onBackPressed() {
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
if (!handled) {
super.onBackPressed()
}
}
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
val backPressable = f as OnBackPressed
if (backPressable.onBackPressed()) {
if (f is OnBackPressed && f.onBackPressed()) {
return true
}
}
@ -388,6 +399,31 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
}
}
/* ==========================================================================================
* User Consent
* ========================================================================================== */
private val consentNotGivenHelper by lazy {
ConsentNotGivenHelper(this, DialogLocker(savedInstanceState))
.apply { restorables.add(this) }
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
consentNotGivenHelper.displayDialog(consentNotGivenError.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
}
/* ==========================================================================================
* Temporary method
* ========================================================================================== */
@ -399,5 +435,4 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
toast(getString(R.string.not_implemented))
}
}
}

View File

@ -19,11 +19,7 @@ package im.vector.riotx.core.platform
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
@ -42,7 +38,7 @@ import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreenInjector {
abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
// Butterknife unbinder
private var mUnBinder: Unbinder? = null
@ -132,10 +128,6 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen
super.onViewStateRestored(savedInstanceState)
}
override fun onBackPressed(): Boolean {
return false
}
override fun invalidate() {
//no-ops by default
Timber.w("invalidate() method has not been implemented")

View File

@ -17,11 +17,8 @@
package im.vector.riotx.core.platform
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.riotx.BuildConfig
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.Disposable
abstract class VectorViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) {

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.services
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.notifications.NotificationUtils
import timber.log.Timber
@ -32,11 +33,18 @@ class CallService : VectorService() {
*/
private var mCallIdInProgress: String? = null
private lateinit var notificationUtils: NotificationUtils
/**
* incoming (foreground notification)
*/
private var mIncomingCallId: String? = null
override fun onCreate() {
super.onCreate()
notificationUtils = vectorComponent().notificationUtils()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
// Service started again by the system.
@ -120,7 +128,7 @@ class CallService : VectorService() {
private fun displayCallInProgressNotification(intent: Intent) {
val callId = intent.getStringExtra(EXTRA_CALL_ID)
val notification = NotificationUtils.buildPendingCallNotification(applicationContext,
val notification = notificationUtils.buildPendingCallNotification(
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
intent.getStringExtra(EXTRA_ROOM_NAME),
intent.getStringExtra(EXTRA_ROOM_ID),
@ -136,7 +144,7 @@ class CallService : VectorService() {
* Hide the permanent call notifications
*/
private fun hideCallNotifications() {
val notification = NotificationUtils.buildCallEndedNotification(applicationContext)
val notification = notificationUtils.buildCallEndedNotification()
// It's mandatory to startForeground to avoid crash
startForeground(NOTIFICATION_ID, notification)

View File

@ -18,11 +18,7 @@ package im.vector.riotx.core.utils
import android.annotation.TargetApi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.*
import android.net.Uri
import android.os.Build
import android.os.PowerManager
@ -32,7 +28,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.features.notifications.supportNotificationChannels
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.settings.VectorLocale
import timber.log.Timber
import java.util.*
@ -138,7 +134,7 @@ fun startNotificationSettingsIntent(activity: AppCompatActivity, requestCode: In
*/
@TargetApi(Build.VERSION_CODES.O)
fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) {
if (!supportNotificationChannels()) return
if (!NotificationUtils.supportNotificationChannels()) return
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelID)

View File

@ -0,0 +1,32 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.utils
import java.lang.ref.WeakReference
import kotlin.reflect.KProperty
fun <T> weak(value: T) = WeakReferenceDelegate(value)
class WeakReferenceDelegate<T>(value: T) {
private var weakReference: WeakReference<T> = WeakReference(value)
operator fun getValue(thisRef: Any, property: KProperty<*>): T? = weakReference.get()
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
weakReference = WeakReference(value)
}
}

View File

@ -19,12 +19,15 @@ package im.vector.riotx.features
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
@ -57,6 +60,7 @@ class MainActivity : VectorBaseActivity() {
@Inject lateinit var matrix: Matrix
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@ -69,42 +73,68 @@ class MainActivity : VectorBaseActivity() {
// Handle some wanted cleanup
if (clearCache || clearCredentials) {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
}
doCleanUp(clearCache, clearCredentials)
} else {
start()
}
}
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
start()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
start()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
else -> start()
}
}
private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
}
start()
}
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) }
.setNegativeButton(R.string.cancel) { _, _ -> start() }
.setCancelable(false)
.show()
}
private fun start() {
val intent = if (sessionHolder.hasActiveSession()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this)
LoginActivity.newIntent(this, null)
}
startActivity(intent)
finish()

View File

@ -0,0 +1,57 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.consent
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.platform.Restorable
import im.vector.riotx.features.webview.VectorWebViewActivity
import im.vector.riotx.features.webview.WebViewMode
class ConsentNotGivenHelper(private val activity: Activity,
private val dialogLocker: DialogLocker) :
Restorable by dialogLocker {
/* ==========================================================================================
* Public methods
* ========================================================================================== */
/**
* Display the consent dialog, if not already displayed
*/
fun displayDialog(consentUri: String, homeServerHost: String) {
dialogLocker.displayDialog {
AlertDialog.Builder(activity)
.setTitle(R.string.settings_app_term_conditions)
.setMessage(activity.getString(R.string.dialog_user_consent_content, homeServerHost))
.setPositiveButton(R.string.dialog_user_consent_submit) { _, _ ->
openWebViewActivity(consentUri)
}
}
}
/* ==========================================================================================
* Private
* ========================================================================================== */
private fun openWebViewActivity(consentUri: String) {
val intent = VectorWebViewActivity.getIntent(activity, consentUri, activity.getString(R.string.settings_app_term_conditions), WebViewMode.CONSENT)
activity.startActivity(intent)
}
}

View File

@ -34,8 +34,8 @@ import im.vector.riotx.core.ui.list.genericItem
import java.util.*
import javax.inject.Inject
class KeysBackupSettingsRecyclerViewController @Inject constructor(val stringProvider: StringProvider,
val session: Session) : TypedEpoxyController<KeysBackupSettingViewState>() {
class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider,
private val session: Session) : TypedEpoxyController<KeysBackupSettingViewState>() {
var listener: Listener? = null

View File

@ -54,7 +54,7 @@ import kotlin.collections.HashMap
*/
@Singleton
class KeyRequestHandler @Inject constructor(val context: Context)
class KeyRequestHandler @Inject constructor(private val context: Context)
: RoomKeysRequestListener,
SasVerificationService.SasVerificationListener {

View File

@ -29,7 +29,7 @@ import javax.inject.Singleton
* Listens to the VerificationManager and add a new notification when an incoming request is detected.
*/
@Singleton
class IncomingVerificationRequestHandler @Inject constructor(val context: Context) : SasVerificationService.SasVerificationListener {
class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : SasVerificationService.SasVerificationListener {
private var session: Session? = null

View File

@ -111,7 +111,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
if (intent.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)) {
if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) {
notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
}
@ -202,10 +202,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
if (!handled) {
super.onBackPressed()
}
super.onBackPressed()
}
}

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
@ -30,7 +29,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
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.SyncState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -208,11 +206,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
syncProgressBar.visibility = when (it.syncState) {
is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
syncStateView.render(it.syncState)
}
companion object {

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
sealed class RoomDetailActions {
data class SaveDraft(val draft: String) : RoomDetailActions()
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
@ -41,9 +42,11 @@ sealed class RoomDetailActions {
object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions()
data class EnterEditMode(val eventId: String) : RoomDetailActions()
data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
data class EnterReplyMode(val eventId: String) : RoomDetailActions()
data class EnterEditMode(val eventId: String, val draft: String) : RoomDetailActions()
data class EnterQuoteMode(val eventId: String, val draft: String) : RoomDetailActions()
data class EnterReplyMode(val eventId: String, val draft: String) : RoomDetailActions()
data class ExitSpecialMode(val draft: String) : RoomDetailActions()
data class ResendMessage(val eventId: String) : RoomDetailActions()
data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
object ClearSendQueue : RoomDetailActions()

View File

@ -44,9 +44,11 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx
@ -118,6 +120,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeSummaryState()
observeJumpToReadMarkerViewVisibility()
observeReadMarkerVisibility()
observeDrafts()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -125,6 +128,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SaveDraft -> handleSaveDraft(action)
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action)
@ -136,6 +140,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ExitSpecialMode -> handleExitSpecialMode(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
@ -156,6 +161,52 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
invisibleEventsObservable.accept(action)
}
/**
* Convert a send mode to a draft and save the draft
*/
private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) {
withState {
when (it.sendMode) {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft))
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
}
}
}
private fun observeDrafts() {
room.rx().liveDrafts()
.subscribe {
Timber.d("Draft update --> SetState")
setState {
val draft = it.lastOrNull() ?: UserDraft.REGULAR("")
copy(
// Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = when (draft) {
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text)
}
}
is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text)
}
}
is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text)
}
}
} ?: SendMode.REGULAR("")
)
}
}
.disposeOnClear()
}
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return
@ -182,22 +233,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun enterEditMode(event: TimelineEvent) {
setState {
copy(
sendMode = SendMode.EDIT(event)
)
}
}
fun resetSendMode() {
setState {
copy(
sendMode = SendMode.REGULAR
)
}
}
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
get() = _nonBlockingPopAlert
@ -234,7 +269,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
withState { state ->
when (state.sendMode) {
SendMode.REGULAR -> {
is SendMode.REGULAR -> {
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
when (slashCommandResult) {
@ -242,6 +277,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft()
}
is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))
@ -254,6 +290,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.SetUserPowerLevel -> {
// TODO
@ -267,6 +304,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
popDraft()
}
is ParsedCommand.UnbanUser -> {
// TODO
@ -291,9 +329,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.ChangeDisplayName -> {
// TODO
@ -301,8 +341,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
}
is SendMode.EDIT -> {
is SendMode.EDIT -> {
//is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
@ -317,21 +356,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
action.text,
action.autoMarkdown)
} else {
Timber.w("Same message content, do not send edition")
}
}
setState {
copy(
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
@ -349,29 +385,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
room.sendFormattedTextMessage(finalText, htmlText)
}
setState {
copy(
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text, action.autoMarkdown)
setState {
copy(
sendMode = SendMode.REGULAR
)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft()
}
}
}
}
}
private fun popDraft() {
room.deleteDraft()
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
var quotedTextMsg = StringBuilder()
@ -485,27 +516,71 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleEditAction(action: RoomDetailActions.EnterEditMode) {
room.getTimeLineEvent(action.eventId)?.let {
enterEditMode(it)
saveCurrentDraft(action.draft)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
timelineEvent.root.eventId?.let {
room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""))
}
}
}
private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) {
room.getTimeLineEvent(action.eventId)?.let {
setState {
copy(
sendMode = SendMode.QUOTE(it)
)
saveCurrentDraft(action.draft)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
withState { state ->
// Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.QUOTE(it, ""))
} else {
room.saveDraft(UserDraft.QUOTE(it, action.draft))
}
}
}
}
}
private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
room.getTimeLineEvent(action.eventId)?.let {
setState {
copy(
sendMode = SendMode.REPLY(it)
)
saveCurrentDraft(action.draft)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
withState { state ->
// Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.REPLY(it, ""))
} else {
room.saveDraft(UserDraft.REPLY(it, action.draft))
}
}
}
}
}
private fun saveCurrentDraft(draft: String) {
// Save the draft with the current text if any
withState {
if (draft.isNotBlank()) {
when (it.sendMode) {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft))
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft))
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft))
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft))
}
}
}
}
private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) {
withState { state ->
// For edit, just delete the current draft
if (state.sendMode is SendMode.EDIT) {
room.deleteDraft()
} else {
// Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.draft))
}
}
}
@ -705,7 +780,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
if (summary.membership == Membership.INVITE) {
summary.latestEvent?.root?.senderId?.let { senderId ->
summary.latestPreviewableEvent?.root?.senderId?.let { senderId ->
session.getUser(senderId)
}?.also {
setState { copy(asyncInviter = Success(it)) }

View File

@ -34,11 +34,11 @@ import im.vector.matrix.android.api.session.user.model.User
*
* Depending on the state the bottom toolbar will change (icons/preview/actions...)
*/
sealed class SendMode {
object REGULAR : SendMode()
data class QUOTE(val timelineEvent: TimelineEvent) : SendMode()
data class EDIT(val timelineEvent: TimelineEvent) : SendMode()
data class REPLY(val timelineEvent: TimelineEvent) : SendMode()
sealed class SendMode(open val text: String) {
data class REGULAR(override val text: String) : SendMode(text)
data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
}
data class RoomDetailViewState(
@ -47,7 +47,7 @@ data class RoomDetailViewState(
val timeline: Timeline? = null,
val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR,
val sendMode: SendMode = SendMode.REGULAR(""),
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized,

View File

@ -25,14 +25,14 @@ class ChronologicalRoomComparator @Inject constructor() : Comparator<RoomSummary
var rightTimestamp = 0L
var leftTimestamp = 0L
if (null != leftRoomSummary) {
leftTimestamp = leftRoomSummary.latestEvent?.root?.originServerTs ?: 0
leftTimestamp = leftRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightTimestamp = rightRoomSummary.latestEvent?.root?.originServerTs ?: 0
rightTimestamp = rightRoomSummary.latestPreviewableEvent?.root?.originServerTs ?: 0
}
return if (rightRoomSummary?.latestEvent?.root == null) {
return if (rightRoomSummary?.latestPreviewableEvent?.root == null) {
-1
} else if (leftRoomSummary?.latestEvent?.root == null) {
} else if (leftRoomSummary?.latestPreviewableEvent?.root == null) {
1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp

View File

@ -32,7 +32,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
@EpoxyAttribute lateinit var title: CharSequence
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var unreadCount: Int = 0
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@ -42,7 +42,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted))
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
holder.titleView.text = title
holder.rootView.setOnClickListener { listener?.invoke() }

View File

@ -27,7 +27,8 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
return false
}
return when (displayMode) {
RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE
RoomListFragment.DisplayMode.HOME ->
roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty()
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN

View File

@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
return true
}
return super.onBackPressed()
return false
}
// RoomSummaryController.Callback **************************************************************

View File

@ -101,7 +101,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
id(titleRes)
title(stringProvider.getString(titleRes).toUpperCase())
expanded(isExpanded)
unreadCount(unreadCount)
unreadNotificationCount(unreadCount)
showHighlighted(showHighlighted)
listener {
mutateExpandedState()

View File

@ -16,9 +16,11 @@
package im.vector.riotx.features.home.room.list
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
@ -36,7 +38,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
@EpoxyAttribute lateinit var lastEventTime: CharSequence
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var unreadCount: Int = 0
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@ -47,14 +51,18 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.titleView.text = roomName
holder.lastEventTimeView.text = lastEventTime
holder.lastEventView.text = lastFormattedEvent
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted))
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
holder.draftView.isVisible = hasDraft
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.roomNameView)
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
val lastEventView by bind<TextView>(R.id.roomLastEventView)
val draftView by bind<ImageView>(R.id.roomDraftBadge)
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider
@ -29,7 +30,6 @@ import im.vector.riotx.core.resources.DateProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import me.gujun.android.span.span
import javax.inject.Inject
@ -59,9 +59,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.latestEvent?.root?.senderId
roomSummary.latestPreviewableEvent?.root?.senderId
} else {
roomSummary.latestEvent?.root?.senderId?.let {
roomSummary.latestPreviewableEvent?.root?.senderId?.let {
stringProvider.getString(R.string.invited_by, it)
}
}
@ -88,13 +88,13 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
var latestFormattedEvent: CharSequence = ""
var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestEvent
val latestEvent = roomSummary.latestPreviewableEvent
if (latestEvent != null) {
val date = latestEvent.root.localDateTime()
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
latestFormattedEvent = if (latestEvent.root.isEncrypted()
&& latestEvent.root.mxDecryptionResult == null) {
&& latestEvent.root.mxDecryptionResult == null) {
stringProvider.getString(R.string.encrypted_message)
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
@ -131,7 +131,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
.roomName(roomSummary.displayName)
.avatarUrl(roomSummary.avatarUrl)
.showHighlighted(showHighlighted)
.unreadCount(unreadCount)
.unreadNotificationCount(unreadCount)
.hasUnreadMessage(roomSummary.hasUnreadMessages)
.hasDraft(roomSummary.userDrafts.isNotEmpty())
.listener { listener?.onRoomSelected(roomSummary) }
}

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