Merge branch 'develop' into feature/read_marker
This commit is contained in:
commit
ea0809ff87
|
@ -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
|
||||
|
||||
|
|
39
CHANGES.md
39
CHANGES.md
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
}
|
|
@ -22,4 +22,5 @@ sealed class SyncState {
|
|||
object PAUSED : SyncState()
|
||||
object KILLING : SyncState()
|
||||
object KILLED : SyncState()
|
||||
object NO_NETWORK : SyncState()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>> {
|
||||
|
|
|
@ -116,6 +116,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
|||
?: return@awaitTransaction
|
||||
roomSummary.notificationCount = 0
|
||||
roomSummary.highlightCount = 0
|
||||
roomSummary.hasUnreadMessages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -3,4 +3,7 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="no_network_indicator">There is no network connection right now</string>
|
||||
</resources>
|
|
@ -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"
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
|
|||
|
||||
@CallSuper
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
session = injector.session()
|
||||
session = injector.activeSessionHolder().getActiveSession()
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)) }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
return true
|
||||
}
|
||||
|
||||
return super.onBackPressed()
|
||||
return false
|
||||
}
|
||||
|
||||
// RoomSummaryController.Callback **************************************************************
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue