forked from GitHub-Mirror/riotX-android
Compare commits
89 Commits
feature/re
...
v0.5.0
Author | SHA1 | Date | |
---|---|---|---|
07fca0922b | |||
ba9d119892 | |||
4453f0ced9 | |||
77168bfd6a | |||
25e9a179d2 | |||
73ec0f5a83 | |||
993fa74252 | |||
38fc4984fe | |||
695d8cce00 | |||
07e99901e1 | |||
20f53e9a58 | |||
ced72aff4f | |||
fdaaca49c2 | |||
3485f023b0 | |||
384dd100e9 | |||
1ba8a58219 | |||
c8010561fc | |||
1f127335bc | |||
138a210a73 | |||
ca6bcde82d | |||
6bda437f5d | |||
3e6b65e174 | |||
137dcab734 | |||
b22b8fba02 | |||
3ccdf4a244 | |||
5fbd271b1c | |||
db8ea0f5e8 | |||
a47a3ead1f | |||
05b2092ffc | |||
6249a59203 | |||
618e9a4f52 | |||
f2c8d4ad02 | |||
be524472ec | |||
1b82a1a24d | |||
cf0b331c3b | |||
2a92a3dc80 | |||
012840abba | |||
a5975a099e | |||
38da4b9ee5 | |||
242e60fcaa | |||
a23be05cbf | |||
ed39b02924 | |||
fe931b5361 | |||
90d9cd0587 | |||
9cedb18921 | |||
e89ba7b87b | |||
902657c22a | |||
eec2abf164 | |||
6879cc8ca8 | |||
fd6bbbd3b5 | |||
0ff0b014a9 | |||
a89f0ddd1d | |||
fdc9e84dd5 | |||
58f878fca9 | |||
88095e4bd9 | |||
47d22a3d5e | |||
28e82cb8ea | |||
35817245cb | |||
75266f42bb | |||
95c4c9ce56 | |||
ce5570105d | |||
188a9aebfa | |||
c95223f5d2 | |||
ef0362ba9c | |||
ea242f6737 | |||
cbc08d834b | |||
0ab6b33fb6 | |||
1b394527b6 | |||
a8f1388721 | |||
166be4e289 | |||
b49ccefe63 | |||
825760d17e | |||
b5af62c3ea | |||
a51d96bf00 | |||
7e142d201d | |||
2be6058971 | |||
49d73f360e | |||
bd88d85a21 | |||
be4fc5cce6 | |||
704da1be55 | |||
5d002532d3 | |||
d4161e9a1a | |||
7966ebef03 | |||
ed5faca5d2 | |||
9cd69d1e33 | |||
456908c851 | |||
215324a03e | |||
02e342849f | |||
df6080b1da |
42
CHANGES.md
42
CHANGES.md
@ -1,23 +1,41 @@
|
||||
Changes in RiotX 0.4.0 (2019-XX-XX)
|
||||
Changes in RiotX 0.5.0 (2019-09-17)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- 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:
|
||||
- Display read receipts in timeline (#81)
|
||||
|
||||
Improvements:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
-
|
||||
- Reactions: Reinstate the ability to react with non-unicode keys (#307)
|
||||
|
||||
Bugfix:
|
||||
-
|
||||
|
||||
Translations:
|
||||
-
|
||||
|
||||
Build:
|
||||
-
|
||||
- Fix text diff linebreak display (#441)
|
||||
- Date change message repeats for each redaction until a normal message (#358)
|
||||
- Slide-in reply icon is distorted (#423)
|
||||
- Regression / e2e replies not encrypted
|
||||
- Some video won't play
|
||||
- Privacy: remove log of notifiable event (#519)
|
||||
- Fix crash with EmojiCompat (#530)
|
||||
|
||||
Changes in RiotX 0.3.0 (2019-08-08)
|
||||
===================================================
|
||||
|
@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
|
||||
vector.debugPrivateData=false
|
||||
vector.httpLogLevel=HEADERS
|
||||
vector.httpLogLevel=NONE
|
||||
|
||||
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
|
||||
#vector.debugPrivateData=true
|
||||
|
@ -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'
|
||||
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CryptoStoreHelper {
|
||||
|
||||
@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
|
||||
}
|
||||
|
||||
fun createCredential() = Credentials(
|
||||
userId = "userId_" + Random().nextInt(),
|
||||
userId = "userId_" + Random.nextInt(),
|
||||
homeServer = "http://matrix.org",
|
||||
accessToken = "access_token",
|
||||
refreshToken = null,
|
||||
|
@ -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
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.api.comparators
|
||||
|
||||
import im.vector.matrix.android.api.interfaces.DatedObject
|
||||
import java.util.*
|
||||
|
||||
object DatedObjectComparators {
|
||||
|
||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
|
@ -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
|
||||
)
|
@ -18,18 +18,17 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
|
||||
class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {
|
||||
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) {
|
||||
|
||||
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveRoomMemberCountCondition(this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "Room member count is $`is`"
|
||||
return "Room member count is $iz"
|
||||
}
|
||||
|
||||
fun isSatisfied(event: Event, session: RoomService?): Boolean {
|
||||
@ -56,12 +55,9 @@ class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_co
|
||||
*/
|
||||
private fun parseIsField(): Pair<String?, Int>? {
|
||||
try {
|
||||
val match = regex.matcher(`is`)
|
||||
if (match.find()) {
|
||||
val prefix = match.group(1)
|
||||
val count = match.group(2).toInt()
|
||||
return prefix to count
|
||||
}
|
||||
val match = regex.find(iz) ?: return null
|
||||
val (prefix, count) = match.destructured
|
||||
return prefix to count.toInt()
|
||||
} catch (t: Throwable) {
|
||||
Timber.d(t)
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ import androidx.lifecycle.LiveData
|
||||
|
||||
interface InitialSyncProgressService {
|
||||
|
||||
fun getLiveStatus() : LiveData<Status?>
|
||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
||||
|
||||
data class Status(
|
||||
@StringRes val statusText: Int?,
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
interface PushersService {
|
||||
|
@ -35,14 +35,9 @@ data class RoomSummary(
|
||||
val highlightCount: Int = 0,
|
||||
val tags: List<RoomTag> = emptyList(),
|
||||
val membership: Membership = Membership.NONE,
|
||||
val versioningState: VersioningState = VersioningState.NONE,
|
||||
val readMarkerId: String? = null
|
||||
val versioningState: VersioningState = VersioningState.NONE
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
get() = versioningState != VersioningState.NONE
|
||||
|
||||
val hasNewMessages: Boolean
|
||||
get() = notificationCount != 0
|
||||
}
|
||||
|
||||
}
|
@ -29,7 +29,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
@ -133,7 +132,7 @@ class CreateRoomParams {
|
||||
)
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(algoEvent)
|
||||
initialStates = mutableListOf(algoEvent)
|
||||
} else {
|
||||
initialStates!!.add(algoEvent)
|
||||
}
|
||||
@ -166,7 +165,7 @@ class CreateRoomParams {
|
||||
content = contentMap.toContent())
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
|
||||
initialStates = mutableListOf(historyVisibilityEvent)
|
||||
} else {
|
||||
initialStates!!.add(historyVisibilityEvent)
|
||||
}
|
||||
|
@ -42,10 +42,5 @@ interface ReadService {
|
||||
|
||||
fun isEventRead(eventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns a nullable read marker for the room.
|
||||
*/
|
||||
fun getReadMarkerLive(): LiveData<String?>
|
||||
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
}
|
@ -32,8 +32,6 @@ interface Timeline {
|
||||
|
||||
var listener: Listener?
|
||||
|
||||
val isLive: Boolean
|
||||
|
||||
/**
|
||||
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
|
||||
*/
|
||||
@ -44,10 +42,6 @@ interface Timeline {
|
||||
*/
|
||||
fun dispose()
|
||||
|
||||
|
||||
fun restartWithEventId(eventId: String)
|
||||
|
||||
|
||||
/**
|
||||
* Check if the timeline can be enriched by paginating.
|
||||
* @param the direction to check in
|
||||
@ -55,7 +49,6 @@ interface Timeline {
|
||||
*/
|
||||
fun hasMoreToLoad(direction: Direction): Boolean
|
||||
|
||||
|
||||
/**
|
||||
* This is the main method to enrich the timeline with new data.
|
||||
* It will call the onUpdated method from [Listener] when the data will be processed.
|
||||
@ -63,16 +56,9 @@ interface Timeline {
|
||||
*/
|
||||
fun paginate(direction: Direction, count: Int)
|
||||
|
||||
fun pendingEventCount(): Int
|
||||
|
||||
fun failedToDeliverEventCount(): Int
|
||||
|
||||
fun getIndexOfEvent(eventId: String?): Int?
|
||||
|
||||
fun getTimelineEventAtIndex(index: Int): TimelineEvent?
|
||||
|
||||
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
|
||||
fun pendingEventCount() : Int
|
||||
|
||||
fun failedToDeliverEventCount() : Int
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
|
@ -39,8 +39,7 @@ data class TimelineEvent(
|
||||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList(),
|
||||
val hasReadMarker: Boolean = false
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
@ -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)
|
||||
@ -105,7 +108,7 @@ internal abstract class CryptoModule {
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService
|
||||
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask
|
||||
|
@ -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
|
||||
@ -93,7 +91,7 @@ import kotlin.math.max
|
||||
* Specially, it tracks all room membership changes events in order to do keys updates.
|
||||
*/
|
||||
@SessionScope
|
||||
internal class CryptoManager @Inject constructor(
|
||||
internal class DefaultCryptoService @Inject constructor(
|
||||
// Olm Manager
|
||||
private val olmManager: OlmManager,
|
||||
// The credentials,
|
||||
@ -135,7 +133,6 @@ internal class CryptoManager @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 CryptoManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
||||
clearCryptoDataTask
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
|
||||
roomDecryptorProvider.addNewSessionListener(newSessionListener)
|
||||
}
|
||||
@ -1067,6 +1056,6 @@ internal class CryptoManager @Inject constructor(
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,
|
||||
|
@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.convertFromUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXOlmDecryption(
|
||||
// The olm device interface
|
||||
@ -158,33 +157,14 @@ internal class MXOlmDecryption(
|
||||
* @return payload, if decrypted successfully.
|
||||
*/
|
||||
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
||||
val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
||||
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet()
|
||||
|
||||
val sessionIds: List<String>
|
||||
|
||||
if (null == sessionIdsSet) {
|
||||
sessionIds = ArrayList()
|
||||
} else {
|
||||
sessionIds = ArrayList(sessionIdsSet)
|
||||
}
|
||||
|
||||
val messageBody = message["body"] as? String
|
||||
var messageType: Int? = null
|
||||
|
||||
val typeAsVoid = message["type"]
|
||||
|
||||
if (null != typeAsVoid) {
|
||||
if (typeAsVoid is Double) {
|
||||
messageType = typeAsVoid.toInt()
|
||||
} else if (typeAsVoid is Int) {
|
||||
messageType = typeAsVoid
|
||||
} else if (typeAsVoid is Long) {
|
||||
messageType = typeAsVoid.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
if (null == messageBody || null == messageType) {
|
||||
return null
|
||||
val messageBody = message["body"] as? String ?: return null
|
||||
val messageType = when (val typeAsVoid = message["type"]) {
|
||||
is Double -> typeAsVoid.toInt()
|
||||
is Int -> typeAsVoid
|
||||
is Long -> typeAsVoid.toInt()
|
||||
else -> return null
|
||||
}
|
||||
|
||||
// Try each session in turn
|
||||
|
@ -26,7 +26,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
@ -59,8 +58,7 @@ object MXEncryptedAttachments {
|
||||
// Half of the IV is random, the lower order bits are zeroed
|
||||
// such that the counter never wraps.
|
||||
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
|
||||
val initVectorBytes = ByteArray(16)
|
||||
Arrays.fill(initVectorBytes, 0.toByte())
|
||||
val initVectorBytes = ByteArray(16) { 0.toByte() }
|
||||
|
||||
val ivRandomPart = ByteArray(8)
|
||||
secureRandom.nextBytes(ivRandomPart)
|
||||
@ -115,7 +113,7 @@ object MXEncryptedAttachments {
|
||||
encryptedByteArray = outStream.toByteArray()
|
||||
)
|
||||
|
||||
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
|
||||
return Try.just(result)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## encryptAttachment failed")
|
||||
@ -206,13 +204,13 @@ object MXEncryptedAttachments {
|
||||
val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
|
||||
outStream.close()
|
||||
|
||||
Timber.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms")
|
||||
|
||||
return decryptedStream
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
|
||||
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptAttachment() : failed " + e.message)
|
||||
Timber.e(e, "## decryptAttachment() : failed ${e.message}")
|
||||
}
|
||||
|
||||
try {
|
||||
@ -228,34 +226,20 @@ object MXEncryptedAttachments {
|
||||
* Base64 URL conversion methods
|
||||
*/
|
||||
|
||||
private fun base64UrlToBase64(base64Url: String?): String? {
|
||||
var result = base64Url
|
||||
if (null != result) {
|
||||
result = result.replace("-".toRegex(), "+")
|
||||
result = result.replace("_".toRegex(), "/")
|
||||
}
|
||||
|
||||
return result
|
||||
private fun base64UrlToBase64(base64Url: String): String {
|
||||
return base64Url.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
}
|
||||
|
||||
private fun base64ToBase64Url(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("\\+".toRegex(), "-")
|
||||
result = result.replace("/".toRegex(), "_")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
return result
|
||||
private fun base64ToBase64Url(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("\\+".toRegex(), "-")
|
||||
.replace('/', '_')
|
||||
.replace("=", "")
|
||||
}
|
||||
|
||||
private fun base64ToUnpaddedBase64(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
|
||||
return result
|
||||
private fun base64ToUnpaddedBase64(base64: String): String {
|
||||
return base64.replace("\n".toRegex(), "")
|
||||
.replace("=", "")
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,8 @@ import org.matrix.olm.OlmPkEncryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
|
||||
@ -114,8 +113,6 @@ internal class KeysBackup @Inject constructor(
|
||||
// The backup key being used.
|
||||
private var backupOlmPkEncryption: OlmPkEncryption? = null
|
||||
|
||||
private val random = Random()
|
||||
|
||||
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
|
||||
|
||||
private var keysBackupStateListener: KeysBackupStateListener? = null
|
||||
@ -848,7 +845,7 @@ internal class KeysBackup @Inject constructor(
|
||||
// Wait between 0 and 10 seconds, to avoid backup requests from
|
||||
// different clients hitting the server all at the same time when a
|
||||
// new key is sent
|
||||
val delayInMs = random.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()
|
||||
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS)
|
||||
|
||||
uiHandler.postDelayed({ backupKeys() }, delayInMs)
|
||||
}
|
||||
@ -1307,7 +1304,7 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
// Make the request
|
||||
storeSessionDataTask
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)){
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) {
|
||||
this.callback = sendingRequestCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@ -1405,7 +1402,7 @@ internal class KeysBackup @Inject constructor(
|
||||
|
||||
companion object {
|
||||
// Maximum delay in ms in {@link maybeBackupKeys}
|
||||
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000
|
||||
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
|
||||
|
||||
// Maximum number of keys to send at a time to the homeserver.
|
||||
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
|
||||
|
@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup
|
||||
import androidx.annotation.WorkerThread
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.experimental.xor
|
||||
|
@ -20,7 +20,6 @@ import android.os.Handler
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class KeysBackupStateManager(private val uiHandler: Handler) {
|
||||
|
||||
|
@ -23,7 +23,6 @@ import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MXDeviceInfo(
|
||||
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.model
|
||||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
data class MXKey(
|
||||
/**
|
||||
@ -46,11 +45,7 @@ data class MXKey(
|
||||
* @return the signed data map
|
||||
*/
|
||||
fun signalableJSONDictionary(): Map<String, Any> {
|
||||
val map = HashMap<String, Any>()
|
||||
|
||||
map["key"] = value
|
||||
|
||||
return map
|
||||
return mapOf("key" to value)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
class MXUsersDevicesMap<E> {
|
||||
|
||||
@ -27,7 +26,7 @@ class MXUsersDevicesMap<E> {
|
||||
* @return the user Ids
|
||||
*/
|
||||
val userIds: List<String>
|
||||
get() = ArrayList(map.keys)
|
||||
get() = map.keys.toList()
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = map.isEmpty()
|
||||
@ -40,7 +39,7 @@ class MXUsersDevicesMap<E> {
|
||||
* @return the device ids list
|
||||
*/
|
||||
fun getUserDeviceIds(userId: String?): List<String>? {
|
||||
return if (userId?.isNotBlank() == true && map.containsKey(userId)) {
|
||||
return if (!userId.isNullOrBlank() && map.containsKey(userId)) {
|
||||
map[userId]!!.keys.toList()
|
||||
} else null
|
||||
}
|
||||
@ -53,7 +52,7 @@ class MXUsersDevicesMap<E> {
|
||||
* @return the object
|
||||
*/
|
||||
fun getObject(userId: String?, deviceId: String?): E? {
|
||||
return if (userId?.isNotBlank() == true && deviceId?.isNotBlank() == true && map.containsKey(userId)) {
|
||||
return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) {
|
||||
map[userId]?.get(deviceId)
|
||||
} else null
|
||||
}
|
||||
@ -67,11 +66,8 @@ class MXUsersDevicesMap<E> {
|
||||
*/
|
||||
fun setObject(userId: String?, deviceId: String?, o: E?) {
|
||||
if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
|
||||
if (map[userId] == null) {
|
||||
map[userId] = HashMap()
|
||||
}
|
||||
|
||||
map[userId]?.put(deviceId, o)
|
||||
val devices = map.getOrPut(userId) { HashMap() }
|
||||
devices[deviceId] = o
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +78,7 @@ class MXUsersDevicesMap<E> {
|
||||
* @param userId the user id
|
||||
*/
|
||||
fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
|
||||
if (userId?.isNotBlank() == true) {
|
||||
if (!userId.isNullOrBlank()) {
|
||||
if (null == objectsPerDevices) {
|
||||
map.remove(userId)
|
||||
} else {
|
||||
@ -97,7 +93,7 @@ class MXUsersDevicesMap<E> {
|
||||
* @param userId the user id.
|
||||
*/
|
||||
fun removeUserObjects(userId: String?) {
|
||||
if (userId?.isNotBlank() == true) {
|
||||
if (!userId.isNullOrBlank()) {
|
||||
map.remove(userId)
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
|
||||
data class Params(
|
||||
@ -45,7 +45,7 @@ internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi
|
||||
return executeRequest {
|
||||
apiCall = cryptoApi.sendToDevice(
|
||||
params.eventType,
|
||||
params.transactionId ?: Random().nextInt(Integer.MAX_VALUE).toString(),
|
||||
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
|
||||
sendToDeviceBody
|
||||
)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@ -161,7 +161,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
|
||||
cancelTransaction(
|
||||
startReq.transactionID!!,
|
||||
otherUserId!!,
|
||||
startReq?.fromDevice ?: event.getSenderKey()!!,
|
||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||
CancelCode.UnknownMethod
|
||||
)
|
||||
}
|
||||
@ -388,14 +388,13 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
|
||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
|
||||
*/
|
||||
private fun createUniqueIDForTransaction(userId: String, deviceID: String): String {
|
||||
val buff = StringBuffer()
|
||||
buff
|
||||
.append(credentials.userId).append("|")
|
||||
.append(credentials.deviceId).append("|")
|
||||
.append(userId).append("|")
|
||||
.append(deviceID).append("|")
|
||||
.append(UUID.randomUUID().toString())
|
||||
return buff.toString()
|
||||
return buildString {
|
||||
append(credentials.userId).append("|")
|
||||
append(credentials.deviceId).append("|")
|
||||
append(userId).append("|")
|
||||
append(deviceID).append("|")
|
||||
append(UUID.randomUUID().toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
@ -158,6 +157,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val eventEntity = TimelineEventEntity(localId).also {
|
||||
it.root = event.toEntity(roomId).apply {
|
||||
this.stateIndex = currentStateIndex
|
||||
@ -169,7 +169,6 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
it.roomId = roomId
|
||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
it.readReceipts = readReceiptsSummaryEntity
|
||||
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
|
||||
}
|
||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||
timelineEvents.add(position, eventEntity)
|
||||
|
@ -22,12 +22,12 @@ 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.*
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomSummaryMapper @Inject constructor(
|
||||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper
|
||||
val cryptoService: CryptoService,
|
||||
val timelineEventMapper: TimelineEventMapper
|
||||
) {
|
||||
|
||||
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
||||
@ -43,12 +43,12 @@ internal class RoomSummaryMapper @Inject constructor(
|
||||
//for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString())
|
||||
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
|
||||
}
|
||||
@ -65,8 +65,7 @@ internal class RoomSummaryMapper @Inject constructor(
|
||||
notificationCount = roomSummaryEntity.notificationCount,
|
||||
tags = tags,
|
||||
membership = roomSummaryEntity.membership,
|
||||
versioningState = roomSummaryEntity.versioningState,
|
||||
readMarkerId = roomSummaryEntity.readMarkerId
|
||||
versioningState = roomSummaryEntity.versioningState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList(),
|
||||
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
||||
var otherMemberIds: RealmList<String> = RealmList(),
|
||||
var notificationCount: Int = 0,
|
||||
var highlightCount: Int = 0,
|
||||
var tags: RealmList<RoomTagEntity> = RealmList(),
|
||||
var readMarkerId: String? = null
|
||||
var tags: RealmList<RoomTagEntity> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
|
@ -43,7 +43,6 @@ import io.realm.annotations.RealmModule
|
||||
PushConditionEntity::class,
|
||||
PusherEntity::class,
|
||||
PusherDataEntity::class,
|
||||
ReadReceiptsSummaryEntity::class,
|
||||
ReadMarkerEntity::class
|
||||
ReadReceiptsSummaryEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
@ -31,8 +31,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
var isUniqueDisplayName: Boolean = false,
|
||||
var senderAvatar: String? = null,
|
||||
var senderMembershipEvent: EventEntity? = null,
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null,
|
||||
var readMarker: ReadMarkerEntity? = null
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("timelineEvents")
|
||||
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.database.query
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
|
||||
val query = realm.where<ReadMarkerEntity>()
|
||||
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
|
||||
if (eventId != null) {
|
||||
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
|
||||
return where(realm, roomId).findFirst()
|
||||
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
|
||||
}
|
@ -20,7 +20,6 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
|
||||
@ -29,12 +28,6 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use
|
||||
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
|
||||
}
|
||||
|
||||
internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
|
||||
return realm.where<ReadReceiptEntity>()
|
||||
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
|
||||
}
|
||||
|
||||
|
||||
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
|
||||
return ReadReceiptEntity().apply {
|
||||
this.primaryKey = "${roomId}_$userId"
|
||||
|
@ -31,12 +31,6 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
|
||||
return query
|
||||
}
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
|
||||
return where(realm, roomId).findFirst()
|
||||
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
|
||||
}
|
||||
|
||||
|
||||
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
|
||||
return RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
|
@ -14,12 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail
|
||||
package im.vector.matrix.android.internal.di
|
||||
|
||||
import java.io.File
|
||||
import javax.inject.Qualifier
|
||||
|
||||
data class DownloadFileState(
|
||||
val mimeType: String,
|
||||
val file: File?,
|
||||
val throwable: Throwable?
|
||||
)
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class UserCacheDirectory
|
@ -18,7 +18,10 @@ 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)
|
||||
internal annotation class MatrixScope
|
||||
annotation class MatrixScope
|
@ -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
|
||||
|
@ -14,12 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.read
|
||||
package im.vector.matrix.android.internal.di
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FullyReadContent(
|
||||
@Json(name = "event_id") val eventId: String
|
||||
)
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class UserMd5
|
@ -18,12 +18,10 @@ package im.vector.matrix.android.internal.network
|
||||
|
||||
import android.content.Context
|
||||
import com.novoda.merlin.Merlin
|
||||
import com.novoda.merlin.MerlinsBeard
|
||||
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 +33,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 = false
|
||||
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 +86,6 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun isConnected(): Boolean {
|
||||
return merlinsBeard.isConnected
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onConnect() {
|
||||
|
||||
|
@ -18,10 +18,12 @@ 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 okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
@ -65,6 +67,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,6 @@ import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.net.ssl.*
|
||||
import kotlin.experimental.and
|
||||
|
||||
|
@ -25,7 +25,6 @@ import java.net.UnknownHostException
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
@ -101,25 +100,16 @@ constructor(trustPinned: Array<TrustManager>, acceptedTlsVersions: List<TlsVersi
|
||||
}
|
||||
|
||||
private fun enableTLSOnSocket(socket: Socket?): Socket? {
|
||||
if (socket != null && socket is SSLSocket) {
|
||||
val sslSocket = socket as SSLSocket?
|
||||
if (socket is SSLSocket) {
|
||||
val supportedProtocols = socket.supportedProtocols.toSet()
|
||||
val filteredEnabledProtocols = enabledProtocols.filter { it in supportedProtocols }
|
||||
|
||||
val supportedProtocols = Arrays.asList(*sslSocket!!.supportedProtocols)
|
||||
val filteredEnabledProtocols = ArrayList<String>()
|
||||
|
||||
for (protocol in enabledProtocols) {
|
||||
if (supportedProtocols.contains(protocol)) {
|
||||
filteredEnabledProtocols.add(protocol)
|
||||
}
|
||||
}
|
||||
|
||||
if (!filteredEnabledProtocols.isEmpty()) {
|
||||
if (filteredEnabledProtocols.isNotEmpty()) {
|
||||
try {
|
||||
sslSocket.enabledProtocols = filteredEnabledProtocols.toTypedArray()
|
||||
socket.enabledProtocols = filteredEnabledProtocols.toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return socket
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package im.vector.matrix.android.internal.session
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
@ -25,31 +26,33 @@ import javax.inject.Inject
|
||||
@SessionScope
|
||||
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
|
||||
|
||||
var status = MutableLiveData<InitialSyncProgressService.Status>()
|
||||
private var status = MutableLiveData<InitialSyncProgressService.Status>()
|
||||
|
||||
var rootTask: TaskInfo? = null
|
||||
private var rootTask: TaskInfo? = null
|
||||
|
||||
override fun getLiveStatus(): LiveData<InitialSyncProgressService.Status?> {
|
||||
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> {
|
||||
return status
|
||||
}
|
||||
|
||||
|
||||
fun startTask(nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
|
||||
fun startTask(@StringRes nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
|
||||
// Create a rootTask, or add a child to the leaf
|
||||
if (rootTask == null) {
|
||||
rootTask = TaskInfo(nameRes, totalProgress)
|
||||
} else {
|
||||
val currentLeaf = rootTask!!.leaf()
|
||||
val newTask = TaskInfo(nameRes, totalProgress)
|
||||
newTask.parent = currentLeaf
|
||||
newTask.offset = currentLeaf.currentProgress
|
||||
|
||||
val newTask = TaskInfo(nameRes,
|
||||
totalProgress,
|
||||
currentLeaf,
|
||||
parentWeight)
|
||||
|
||||
currentLeaf.child = newTask
|
||||
newTask.parentWeight = parentWeight
|
||||
}
|
||||
reportProgress(0)
|
||||
}
|
||||
|
||||
fun reportProgress(progress: Int) {
|
||||
rootTask?.leaf()?.incrementProgress(progress)
|
||||
rootTask?.leaf()?.setProgress(progress)
|
||||
}
|
||||
|
||||
fun endTask(nameRes: Int) {
|
||||
@ -58,7 +61,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||
//close it
|
||||
val parent = endedTask.parent
|
||||
parent?.child = null
|
||||
parent?.incrementProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
|
||||
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
|
||||
}
|
||||
if (endedTask?.parent == null) {
|
||||
status.postValue(null)
|
||||
@ -71,14 +74,17 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||
}
|
||||
|
||||
|
||||
inner class TaskInfo(var nameRes: Int,
|
||||
var totalProgress: Int) {
|
||||
var parent: TaskInfo? = null
|
||||
private inner class TaskInfo(@StringRes var nameRes: Int,
|
||||
var totalProgress: Int,
|
||||
var parent: TaskInfo? = null,
|
||||
var parentWeight: Float = 1f,
|
||||
var offset: Int = parent?.currentProgress ?: 0) {
|
||||
var child: TaskInfo? = null
|
||||
var parentWeight: Float = 1f
|
||||
var currentProgress: Int = 0
|
||||
var offset: Int = 0
|
||||
|
||||
/**
|
||||
* Get the further child
|
||||
*/
|
||||
fun leaf(): TaskInfo {
|
||||
var last = this
|
||||
while (last.child != null) {
|
||||
@ -87,26 +93,27 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||
return last
|
||||
}
|
||||
|
||||
fun incrementProgress(progress: Int) {
|
||||
/**
|
||||
* Set progress of the parent if any (which will post value), or post the value
|
||||
*/
|
||||
fun setProgress(progress: Int) {
|
||||
currentProgress = progress
|
||||
// val newProgress = Math.min(currentProgress + progress, totalProgress)
|
||||
parent?.let {
|
||||
val parentProgress = (currentProgress * parentWeight).toInt()
|
||||
it.incrementProgress(offset + parentProgress)
|
||||
}
|
||||
if (parent == null) {
|
||||
Timber.e("--- ${leaf().nameRes}: ${currentProgress}")
|
||||
it.setProgress(offset + parentProgress)
|
||||
} ?: run {
|
||||
Timber.e("--- ${leaf().nameRes}: $currentProgress")
|
||||
status.postValue(
|
||||
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline fun <T> reportSubtask(reporter: DefaultInitialSyncProgressService?,
|
||||
nameRes: Int,
|
||||
@StringRes nameRes: Int,
|
||||
totalProgress: Int,
|
||||
parentWeight: Float = 1f,
|
||||
block: () -> T): T {
|
||||
@ -121,11 +128,11 @@ inline fun <K, V, R> Map<out K, V>.mapWithProgress(reporter: DefaultInitialSyncP
|
||||
taskId: Int,
|
||||
weight: Float,
|
||||
transform: (Map.Entry<K, V>) -> R): List<R> {
|
||||
val total = count()
|
||||
val total = count().toFloat()
|
||||
var current = 0
|
||||
reporter?.startTask(taskId, 100, weight)
|
||||
return this.map {
|
||||
reporter?.reportProgress((current / total.toFloat() * 100).toInt())
|
||||
return map {
|
||||
reporter?.reportProgress((current / total * 100).toInt())
|
||||
current++
|
||||
transform.invoke(it)
|
||||
}.also {
|
||||
|
@ -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.CryptoManager
|
||||
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
|
||||
@ -63,8 +62,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||
private val signOutService: Lazy<SignOutService>,
|
||||
private val pushRuleService: Lazy<PushRuleService>,
|
||||
private val pushersService: Lazy<PushersService>,
|
||||
private val cryptoService: Lazy<CryptoManager>,
|
||||
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
|
||||
|
||||
}
|
||||
|
@ -21,4 +21,4 @@ import javax.inject.Scope
|
||||
@Scope
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class SessionScope
|
||||
annotation class SessionScope
|
@ -30,9 +30,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
||||
private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
|
||||
|
||||
override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||
val listeners = listeners[key] ?: ArrayList()
|
||||
val listeners = listeners.getOrPut(key) { ArrayList() }
|
||||
listeners.add(updateListener)
|
||||
this.listeners[key] = listeners
|
||||
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
|
||||
mainHandler.post { updateListener.onUpdate(currentState) }
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -28,10 +28,8 @@ import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
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.model.RoomSummaryEntity
|
||||
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.where
|
||||
@ -95,15 +93,6 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
|
||||
return isEventRead
|
||||
}
|
||||
|
||||
override fun getReadMarkerLive(): LiveData<String?> {
|
||||
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
ReadMarkerEntity.where(realm, roomId)
|
||||
}
|
||||
return Transformations.map(liveRealmData) { results ->
|
||||
results.firstOrNull()?.eventId
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
|
||||
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
ReadReceiptsSummaryEntity.where(realm, eventId)
|
||||
|
@ -18,25 +18,19 @@ package im.vector.matrix.android.internal.session.room.read
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
||||
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.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
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.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -55,8 +49,7 @@ private const val READ_RECEIPT = "m.read"
|
||||
|
||||
internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI,
|
||||
private val credentials: Credentials,
|
||||
private val monarchy: Monarchy,
|
||||
private val roomFullyReadHandler: RoomFullyReadHandler
|
||||
private val monarchy: Monarchy
|
||||
) : SetReadMarkersTask {
|
||||
|
||||
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
||||
@ -64,7 +57,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
val fullyReadEventId: String?
|
||||
val readReceiptEventId: String?
|
||||
|
||||
Timber.v("Execute set read marker with params: $params")
|
||||
if (params.markAllAsRead) {
|
||||
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
||||
@ -76,16 +68,16 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
readReceiptEventId = params.readReceiptEventId
|
||||
}
|
||||
|
||||
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
||||
if (fullyReadEventId != null) {
|
||||
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
|
||||
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
|
||||
} else {
|
||||
updateReadMarker(params.roomId, fullyReadEventId)
|
||||
markers[READ_MARKER] = fullyReadEventId
|
||||
}
|
||||
}
|
||||
if (readReceiptEventId != null
|
||||
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
||||
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
||||
|
||||
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
|
||||
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
|
||||
} else {
|
||||
@ -101,30 +93,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()
|
||||
val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull()
|
||||
val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst()
|
||||
val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
|
||||
val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE
|
||||
eventToCheckIndex > readReceiptIndex
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateReadMarker(roomId: String, eventId: String) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
|
||||
if (isLatestReceived) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: return@awaitTransaction
|
||||
?: return@writeAsync
|
||||
roomSummary.notificationCount = 0
|
||||
roomSummary.highlightCount = 0
|
||||
}
|
||||
@ -132,17 +106,19 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
}
|
||||
|
||||
private fun isEventRead(roomId: String, eventId: String): Boolean {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
|
||||
?: return false
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return false
|
||||
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
|
||||
?: Int.MIN_VALUE
|
||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||
?: Int.MAX_VALUE
|
||||
eventToCheckIndex <= readReceiptIndex
|
||||
?: Int.MAX_VALUE
|
||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||
}
|
||||
return isEventRead
|
||||
}
|
||||
|
||||
}
|
@ -69,15 +69,11 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val sendRelationWork = createSendRelationWork(event)
|
||||
val sendRelationWork = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
|
||||
return CancelableWork(context, sendRelationWork.id)
|
||||
}
|
||||
|
||||
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
||||
return createSendEventWork(event)
|
||||
}
|
||||
|
||||
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
||||
|
||||
val params = FindReactionEventForUndoTask.Params(
|
||||
@ -134,42 +130,42 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
|
||||
return CancelableWork(context, encryptWork.id)
|
||||
CancelableWork(context, encryptWork.id)
|
||||
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||
return CancelableWork(context, workRequest.id)
|
||||
CancelableWork(context, workRequest.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun editReply(replyToEdit: TimelineEvent,
|
||||
originalEvent: TimelineEvent,
|
||||
originalTimelineEvent: TimelineEvent,
|
||||
newBodyText: String,
|
||||
compatibilityBodyText: String): Cancelable {
|
||||
val event = eventFactory
|
||||
.createReplaceTextOfReply(roomId,
|
||||
replyToEdit,
|
||||
originalEvent,
|
||||
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
|
||||
replyToEdit,
|
||||
originalTimelineEvent,
|
||||
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
|
||||
return CancelableWork(context, encryptWork.id)
|
||||
CancelableWork(context, encryptWork.id)
|
||||
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||
return CancelableWork(context, workRequest.id)
|
||||
CancelableWork(context, workRequest.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,16 +183,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
saveLocalEcho(it)
|
||||
} ?: return null
|
||||
|
||||
if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
|
||||
return CancelableWork(context, encryptWork.id)
|
||||
CancelableWork(context, encryptWork.id)
|
||||
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event)
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||
return CancelableWork(context, workRequest.id)
|
||||
CancelableWork(context, workRequest.id)
|
||||
}
|
||||
|
||||
}
|
||||
@ -208,10 +204,10 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event): OneTimeWorkRequest {
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, true)
|
||||
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
||||
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> {
|
||||
@ -220,7 +216,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
}
|
||||
return Transformations.map(liveEntity) { realmResults ->
|
||||
realmResults.firstOrNull()?.asDomain()
|
||||
?: EventAnnotationsSummary(eventId, emptyList(), null)
|
||||
?: EventAnnotationsSummary(eventId, emptyList(), null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,7 +229,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||
private fun saveLocalEcho(event: Event) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||
?: return@writeAsync
|
||||
?: return@writeAsync
|
||||
roomEntity.addSendingEvent(event)
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.util.StringProvider
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -304,17 +304,22 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
}
|
||||
|
||||
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
|
||||
val lines = body.text.split("\n")
|
||||
val replyFallback = StringBuffer("> <$originalSenderId>")
|
||||
lines.forEachIndexed { index, s ->
|
||||
if (index == 0) {
|
||||
replyFallback.append(" $s")
|
||||
} else {
|
||||
replyFallback.append("\n> $s")
|
||||
return buildString {
|
||||
append("> <")
|
||||
append(originalSenderId)
|
||||
append(">")
|
||||
|
||||
val lines = body.text.split("\n")
|
||||
lines.forEachIndexed { index, s ->
|
||||
if (index == 0) {
|
||||
append(" $s")
|
||||
} else {
|
||||
append("\n> $s")
|
||||
}
|
||||
}
|
||||
append("\n\n")
|
||||
append(newBodyText)
|
||||
}
|
||||
replyFallback.append("\n\n").append(newBodyText)
|
||||
return replyFallback.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,15 +27,33 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.*
|
||||
import im.vector.matrix.android.internal.database.query.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
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.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.FilterContent
|
||||
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
||||
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.database.query.whereInRoom
|
||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.Debouncer
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import im.vector.matrix.android.internal.util.createUIHandler
|
||||
import io.realm.*
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@ -49,7 +67,7 @@ private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||
|
||||
internal class DefaultTimeline(
|
||||
private val roomId: String,
|
||||
private var initialEventId: String? = null,
|
||||
private val initialEventId: String? = null,
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
@ -57,9 +75,8 @@ internal class DefaultTimeline(
|
||||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val settings: TimelineSettings,
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
||||
private val hiddenReadMarker: TimelineHiddenReadMarker
|
||||
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||
|
||||
private companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||
@ -87,6 +104,7 @@ internal class DefaultTimeline(
|
||||
|
||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private val isLive = initialEventId == null
|
||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||
@ -94,9 +112,6 @@ internal class DefaultTimeline(
|
||||
|
||||
private val timelineID = UUID.randomUUID().toString()
|
||||
|
||||
override val isLive
|
||||
get() = initialEventId == null
|
||||
|
||||
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
|
||||
|
||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
|
||||
@ -105,7 +120,10 @@ internal class DefaultTimeline(
|
||||
} else {
|
||||
// If changeSet has deletion we are having a gap, so we clear everything
|
||||
if (changeSet.deletionRanges.isNotEmpty()) {
|
||||
clearAllValues()
|
||||
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||
builtEvents.clear()
|
||||
builtEventsIdMap.clear()
|
||||
}
|
||||
changeSet.insertionRanges.forEach { range ->
|
||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||
@ -131,9 +149,13 @@ internal class DefaultTimeline(
|
||||
changeSet.changes.forEach { index ->
|
||||
val eventEntity = results[index]
|
||||
eventEntity?.eventId?.let { eventId ->
|
||||
hasChanged = rebuildEvent(eventId) {
|
||||
buildTimelineEvent(eventEntity)
|
||||
} || hasChanged
|
||||
builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update an existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasChanged) postSnapshot()
|
||||
@ -141,21 +163,27 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
|
||||
|
||||
var hasChange = false
|
||||
|
||||
(changeSet.insertions + changeSet.changes).forEach {
|
||||
val eventRelations = collection[it]
|
||||
if (eventRelations != null) {
|
||||
hasChange = rebuildEvent(eventRelations.eventId) { te ->
|
||||
te.copy(annotations = eventRelations.asDomain())
|
||||
} || hasChange
|
||||
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain())
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasChange) postSnapshot()
|
||||
if (hasChange)
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
|
||||
// Public methods ******************************************************************************
|
||||
// Public methods ******************************************************************************
|
||||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
@ -212,7 +240,7 @@ internal class DefaultTimeline(
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.start(realm, liveEvents, this)
|
||||
}
|
||||
hiddenReadMarker.start(realm, liveEvents, this)
|
||||
|
||||
isReady.set(true)
|
||||
}
|
||||
}
|
||||
@ -227,11 +255,9 @@ internal class DefaultTimeline(
|
||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||
eventRelations.removeAllChangeListeners()
|
||||
liveEvents.removeAllChangeListeners()
|
||||
hiddenReadMarker.dispose()
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.dispose()
|
||||
}
|
||||
clearAllValues()
|
||||
backgroundRealm.getAndSet(null).also {
|
||||
it.close()
|
||||
}
|
||||
@ -239,27 +265,6 @@ internal class DefaultTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
override fun restartWithEventId(eventId: String) {
|
||||
dispose()
|
||||
initialEventId = eventId
|
||||
start()
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
override fun getIndexOfEvent(eventId: String?): Int? {
|
||||
return builtEventsIdMap[eventId]
|
||||
}
|
||||
|
||||
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
|
||||
return builtEvents.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
|
||||
return builtEventsIdMap[eventId]?.let {
|
||||
getTimelineEventAtIndex(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
@ -267,38 +272,20 @@ internal class DefaultTimeline(
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
te.copy(readReceipts = readReceipts)
|
||||
}
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun onReadReceiptsUpdated() {
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// TimelineHiddenReadMarker.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
te.copy(hasReadMarker = hasReadMarker)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReadMarkerUpdated() {
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = builder(te)
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||
@ -408,9 +395,8 @@ internal class DefaultTimeline(
|
||||
|
||||
prevDisplayIndex = initialDisplayIndex
|
||||
nextDisplayIndex = initialDisplayIndex
|
||||
val currentInitialEventId = initialEventId
|
||||
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
||||
fetchEvent(currentInitialEventId)
|
||||
if (initialEventId != null && shouldFetchInitialEvent) {
|
||||
fetchEvent(initialEventId)
|
||||
} else {
|
||||
val count = Math.min(settings.initialSize, liveEvents.size)
|
||||
if (isLive) {
|
||||
@ -557,11 +543,10 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
private fun findCurrentChunk(realm: Realm): ChunkEntity? {
|
||||
val currentInitialEventId = initialEventId
|
||||
return if (currentInitialEventId == null) {
|
||||
return if (initialEventId == null) {
|
||||
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
} else {
|
||||
ChunkEntity.findIncludingEvent(realm, currentInitialEventId)
|
||||
ChunkEntity.findIncludingEvent(realm, initialEventId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,24 +566,12 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
private fun postSnapshot() {
|
||||
BACKGROUND_HANDLER.post {
|
||||
val snapshot = createSnapshot()
|
||||
val runnable = Runnable { listener?.onUpdated(snapshot) }
|
||||
debouncer.debounce("post_snapshot", runnable, 50)
|
||||
}
|
||||
val snapshot = createSnapshot()
|
||||
val runnable = Runnable { listener?.onUpdated(snapshot) }
|
||||
debouncer.debounce("post_snapshot", runnable, 50)
|
||||
}
|
||||
|
||||
private fun clearAllValues() {
|
||||
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||
builtEvents.clear()
|
||||
builtEventsIdMap.clear()
|
||||
backwardsPaginationState.set(PaginationState())
|
||||
forwardsPaginationState.set(PaginationState())
|
||||
}
|
||||
|
||||
|
||||
// Extension methods ***************************************************************************
|
||||
// Extension methods ***************************************************************************
|
||||
|
||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||
|
@ -59,8 +59,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
settings,
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
TimelineHiddenReadMarker(roomId)
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
/*
|
||||
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmObjectChangeListener
|
||||
import io.realm.RealmResults
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the read marker for hidden events.
|
||||
* When an hidden event has read marker, we want to transfer it on the first older displayed event.
|
||||
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
|
||||
*/
|
||||
internal class TimelineHiddenReadMarker constructor(private val roomId: String) {
|
||||
|
||||
interface Delegate {
|
||||
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
|
||||
fun onReadMarkerUpdated()
|
||||
}
|
||||
|
||||
private var previousDisplayedEventId: String? = null
|
||||
private var readMarkerEntity: ReadMarkerEntity? = null
|
||||
|
||||
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var delegate: Delegate
|
||||
|
||||
private val readMarkerListener = RealmObjectChangeListener<ReadMarkerEntity> { readMarker, _ ->
|
||||
var hasChange = false
|
||||
previousDisplayedEventId?.also {
|
||||
hasChange = delegate.rebuildEvent(it, false)
|
||||
previousDisplayedEventId = null
|
||||
}
|
||||
val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null
|
||||
if (isEventHidden) {
|
||||
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
|
||||
?: return@RealmObjectChangeListener
|
||||
val displayIndex = hiddenEvent.root?.displayIndex
|
||||
if (displayIndex != null) {
|
||||
// Then we are looking for the first displayable event after the hidden one
|
||||
val firstDisplayedEvent = liveEvents.where()
|
||||
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
|
||||
.findFirst()
|
||||
|
||||
// If we find one, we should rebuild this one with marker
|
||||
if (firstDisplayedEvent != null) {
|
||||
previousDisplayedEventId = firstDisplayedEvent.eventId
|
||||
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasChange) delegate.onReadMarkerUpdated()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun start(realm: Realm, liveEvents: RealmResults<TimelineEventEntity>, delegate: Delegate) {
|
||||
this.liveEvents = liveEvents
|
||||
this.delegate = delegate
|
||||
// We are looking for read receipts set on hidden events.
|
||||
// We only accept those with a timelineEvent (so coming from pagination/sync).
|
||||
readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId)
|
||||
.findFirstAsync()
|
||||
.also { it.addChangeListener(readMarkerListener) }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun dispose() {
|
||||
this.readMarkerEntity?.removeAllChangeListeners()
|
||||
}
|
||||
|
||||
}
|
@ -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,10 +22,12 @@ 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.*
|
||||
import javax.crypto.*
|
||||
@ -33,6 +35,7 @@ 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,19 +473,14 @@ 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)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
CipherInputStream(encrypted, output).use {
|
||||
it.copyTo(bos)
|
||||
}
|
||||
|
||||
return bos.toByteArray()
|
||||
return CipherInputStream(encrypted, output).use { it.readBytes() }
|
||||
}
|
||||
|
||||
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
|
||||
@ -495,14 +491,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv, 0, ivSize)
|
||||
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Pair(iv, encrypted)
|
||||
}
|
||||
|
||||
@ -516,7 +505,6 @@ object SecretStoringUtils {
|
||||
}
|
||||
|
||||
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_1)
|
||||
|
||||
@ -530,14 +518,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Triple(encryptedKey, iv, encrypted)
|
||||
}
|
||||
|
||||
@ -567,7 +548,6 @@ object SecretStoringUtils {
|
||||
}
|
||||
|
||||
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_2)
|
||||
|
||||
@ -579,14 +559,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Triple(salt, iv, encrypted)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ 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.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
@ -33,7 +33,7 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
internal class CryptoSyncHandler @Inject constructor(private val cryptoManager: CryptoManager,
|
||||
internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
|
||||
private val sasVerificationService: DefaultSasVerificationService) {
|
||||
|
||||
fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) {
|
||||
@ -47,13 +47,13 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoManager:
|
||||
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content)
|
||||
} else {
|
||||
sasVerificationService.onToDeviceEvent(event)
|
||||
cryptoManager.onToDeviceEvent(event)
|
||||
cryptoService.onToDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSyncCompleted(syncResponse: SyncResponse) {
|
||||
cryptoManager.onSyncCompleted(syncResponse)
|
||||
cryptoService.onSyncCompleted(syncResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoManager:
|
||||
if (event.getClearType() == EventType.ENCRYPTED) {
|
||||
var result: MXEventDecryptionResult? = null
|
||||
try {
|
||||
result = cryptoManager.decryptEvent(event, timelineId ?: "")
|
||||
result = cryptoService.decryptEvent(event, timelineId ?: "")
|
||||
} catch (exception: MXCryptoError) {
|
||||
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType //setCryptoError(exception.cryptoError)
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.sync
|
||||
|
||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomFullyReadHandler @Inject constructor() {
|
||||
|
||||
fun handle(realm: Realm, roomId: String, content: FullyReadContent?) {
|
||||
if (content == null) {
|
||||
return
|
||||
}
|
||||
Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}")
|
||||
|
||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||
readMarkerId = content.eventId
|
||||
}
|
||||
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
||||
eventId = content.eventId
|
||||
}
|
||||
|
||||
// Remove the old marker if any
|
||||
readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null
|
||||
// Attach to timelineEvent if known
|
||||
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
|
||||
timelineEventEntity?.readMarker = readMarkerEntity
|
||||
}
|
||||
|
||||
}
|
@ -23,13 +23,8 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
@ -42,11 +37,7 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer
|
||||
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
@ -59,8 +50,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val roomTagHandler: RoomTagHandler,
|
||||
private val roomFullyReadHandler: RoomFullyReadHandler,
|
||||
private val cryptoManager: CryptoManager,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
private val tokenStore: SyncTokenStore,
|
||||
private val pushRuleService: DefaultPushRuleService,
|
||||
private val processForPushTask: ProcessEventForPushTask,
|
||||
@ -107,12 +97,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
handleJoinedRoom(realm, it.key, it.value, isInitialSync)
|
||||
}
|
||||
is HandlingStrategy.INVITED ->
|
||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
|
||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) {
|
||||
handleInvitedRoom(realm, it.key, it.value)
|
||||
}
|
||||
|
||||
is HandlingStrategy.LEFT -> {
|
||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.2f) {
|
||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) {
|
||||
handleLeftRoom(realm, it.key, it.value)
|
||||
}
|
||||
}
|
||||
@ -135,8 +125,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||
}
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
|
||||
if (roomEntity.membership == Membership.INVITE) {
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
@ -145,13 +134,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
|
||||
// State event
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||
?: Int.MIN_VALUE
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() ?: Int.MIN_VALUE
|
||||
val untimelinedStateIndex = minStateIndex + 1
|
||||
roomSync.state.events.forEach { event ->
|
||||
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||
// Give info to crypto module
|
||||
cryptoManager.onStateEvent(roomId, event)
|
||||
cryptoService.onStateEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
@ -177,8 +165,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
roomSync:
|
||||
InvitedRoomSync): RoomEntity {
|
||||
Timber.v("Handle invited sync for room $roomId")
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
roomEntity.membership = Membership.INVITE
|
||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||
@ -191,8 +178,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
private fun handleLeftRoom(realm: Realm,
|
||||
roomId: String,
|
||||
roomSync: RoomSync): RoomEntity {
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
|
||||
roomEntity.membership = Membership.LEAVE
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
@ -224,7 +210,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
event.eventId?.also { eventIds.add(it) }
|
||||
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
// Give info to crypto module
|
||||
cryptoManager.onLiveEvent(roomEntity.roomId, event)
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
@ -257,16 +243,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
for (event in accountData.events) {
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
roomTagHandler.handle(realm, roomId, content)
|
||||
} else if (eventType == EventType.FULLY_READ) {
|
||||
val content = event.getClearContent().toModel<FullyReadContent>()
|
||||
roomFullyReadHandler.handle(realm, roomId, content)
|
||||
}
|
||||
}
|
||||
accountData.events
|
||||
.asSequence()
|
||||
.filter { it.getClearType() == EventType.TAG }
|
||||
.map { it.content.toModel<RoomTagContent>() }
|
||||
.forEach { roomTagHandler.handle(realm, roomId, it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomTagEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomTagHandler @Inject constructor() {
|
||||
@ -30,16 +29,8 @@ internal class RoomTagHandler @Inject constructor() {
|
||||
if (content == null) {
|
||||
return
|
||||
}
|
||||
val tags = ArrayList<RoomTagEntity>()
|
||||
for (tagName in content.tags.keys) {
|
||||
val params = content.tags[tagName]
|
||||
val order = params?.get("order")
|
||||
val tag = if (order is Double) {
|
||||
RoomTagEntity(tagName, order)
|
||||
} else {
|
||||
RoomTagEntity(tagName, null)
|
||||
}
|
||||
tags.add(tag)
|
||||
val tags = content.tags.entries.map { (tagName, params) ->
|
||||
RoomTagEntity(tagName, params["order"] as? Double)
|
||||
}
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: RoomSummaryEntity(roomId)
|
||||
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.sync
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
|
||||
import im.vector.matrix.android.internal.session.reportSubtask
|
||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
@ -30,7 +30,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
|
||||
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
||||
private val groupSyncHandler: GroupSyncHandler,
|
||||
private val cryptoSyncHandler: CryptoSyncHandler,
|
||||
private val cryptoManager: CryptoManager,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
private val initialSyncProgressService: DefaultInitialSyncProgressService) {
|
||||
|
||||
fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> {
|
||||
@ -40,12 +40,12 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
|
||||
val reporter = initialSyncProgressService.takeIf { isInitialSync }
|
||||
|
||||
measureTimeMillis {
|
||||
if (!cryptoManager.isStarted()) {
|
||||
Timber.v("Should start cryptoManager")
|
||||
cryptoManager.start(isInitialSync)
|
||||
if (!cryptoService.isStarted()) {
|
||||
Timber.v("Should start cryptoService")
|
||||
cryptoService.start(isInitialSync)
|
||||
}
|
||||
}.also {
|
||||
Timber.v("Finish handling start cryptoManager in $it ms")
|
||||
Timber.v("Finish handling start cryptoService in $it ms")
|
||||
}
|
||||
val measure = measureTimeMillis {
|
||||
// Handle the to device events before the room ones
|
||||
|
@ -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,7 +19,7 @@ package im.vector.matrix.android.internal.util
|
||||
import android.content.Context
|
||||
import androidx.work.WorkManager
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
internal class CancelableWork(private val context: Context,
|
||||
private val workId: UUID) : Cancelable {
|
||||
|
@ -34,7 +34,7 @@ import java.security.*
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
@ -22,7 +22,7 @@ import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.TreeSet
|
||||
|
||||
/**
|
||||
* Build canonical Json
|
||||
@ -60,43 +60,33 @@ object JsonCanonicalizer {
|
||||
when (any) {
|
||||
is JSONArray -> {
|
||||
// Canonicalize each element of the array
|
||||
val result = StringBuilder("[")
|
||||
|
||||
for (i in 0 until any.length()) {
|
||||
result.append(canonicalizeRecursive(any.get(i)))
|
||||
if (i < any.length() - 1) {
|
||||
result.append(",")
|
||||
}
|
||||
return (0 until any.length()).joinToString(separator = ",", prefix = "[", postfix = "]") {
|
||||
canonicalizeRecursive(any.get(it))
|
||||
}
|
||||
|
||||
result.append("]")
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
is JSONObject -> {
|
||||
// Sort the attributes by name, and the canonicalize each element of the JSONObject
|
||||
val result = StringBuilder("{")
|
||||
|
||||
val attributes = TreeSet<String>()
|
||||
for (entry in any.keys()) {
|
||||
attributes.add(entry)
|
||||
}
|
||||
|
||||
for (attribute in attributes.withIndex()) {
|
||||
result.append("\"")
|
||||
.append(attribute.value)
|
||||
.append("\"")
|
||||
.append(":")
|
||||
.append(canonicalizeRecursive(any[attribute.value]))
|
||||
return buildString {
|
||||
append("{")
|
||||
for ((index, value) in attributes.withIndex()) {
|
||||
append("\"")
|
||||
append(value)
|
||||
append("\"")
|
||||
append(":")
|
||||
append(canonicalizeRecursive(any[value]))
|
||||
|
||||
if (attribute.index < attributes.size - 1) {
|
||||
result.append(",")
|
||||
if (index < attributes.size - 1) {
|
||||
append(",")
|
||||
}
|
||||
}
|
||||
append("}")
|
||||
}
|
||||
|
||||
result.append("}")
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
is String -> return JSONObject.quote(any)
|
||||
else -> return any.toString()
|
||||
|
@ -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"
|
||||
|
||||
|
@ -167,4 +167,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Начална синхронизация:
|
||||
\nИмпортиране на данни за профила</string>
|
||||
|
||||
<string name="notice_room_update">%s обнови тази стая.</string>
|
||||
|
||||
<string name="event_status_sending_message">Изпращане на съобщение…</string>
|
||||
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>
|
||||
|
||||
</resources>
|
||||
|
@ -105,4 +105,65 @@
|
||||
<string name="verification_emoji_pig">Schwein</string>
|
||||
<string name="verification_emoji_elephant">Elefant</string>
|
||||
<string name="verification_emoji_rabbit">Hase</string>
|
||||
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
|
||||
|
||||
<string name="verification_emoji_panda">Panda</string>
|
||||
<string name="verification_emoji_rooster">Hahn</string>
|
||||
<string name="verification_emoji_penguin">Pinguin</string>
|
||||
<string name="verification_emoji_turtle">Schildkröte</string>
|
||||
<string name="verification_emoji_fish">Fisch</string>
|
||||
<string name="verification_emoji_octopus">Tintenfisch</string>
|
||||
<string name="verification_emoji_butterfly">Schmetterling</string>
|
||||
<string name="verification_emoji_flower">Blume</string>
|
||||
<string name="verification_emoji_tree">Baum</string>
|
||||
<string name="verification_emoji_cactus">Kaktus</string>
|
||||
<string name="verification_emoji_mushroom">Pilz</string>
|
||||
<string name="verification_emoji_globe">Globus</string>
|
||||
<string name="verification_emoji_moon">Mond</string>
|
||||
<string name="verification_emoji_cloud">Wolke</string>
|
||||
<string name="verification_emoji_fire">Feuer</string>
|
||||
<string name="verification_emoji_banana">Banane</string>
|
||||
<string name="verification_emoji_apple">Apfel</string>
|
||||
<string name="verification_emoji_strawberry">Erdbeere</string>
|
||||
<string name="verification_emoji_corn">Mais</string>
|
||||
<string name="verification_emoji_cake">Kuchen</string>
|
||||
<string name="verification_emoji_heart">Herz</string>
|
||||
<string name="verification_emoji_smiley">Lächeln</string>
|
||||
<string name="verification_emoji_robot">Roboter</string>
|
||||
<string name="verification_emoji_hat">Hut</string>
|
||||
<string name="verification_emoji_glasses">Brille</string>
|
||||
<string name="verification_emoji_wrench">Schraubenschlüssel</string>
|
||||
<string name="verification_emoji_santa">Nikolaus</string>
|
||||
<string name="verification_emoji_thumbsup">Daumen hoch</string>
|
||||
<string name="verification_emoji_umbrella">Regenschirm</string>
|
||||
<string name="verification_emoji_hourglass">Sanduhr</string>
|
||||
<string name="verification_emoji_clock">Uhr</string>
|
||||
<string name="verification_emoji_gift">Geschenk</string>
|
||||
<string name="verification_emoji_lightbulb">Glühbirne</string>
|
||||
<string name="verification_emoji_book">Buch</string>
|
||||
<string name="verification_emoji_pencil">Stift</string>
|
||||
<string name="verification_emoji_paperclip">Büroklammer</string>
|
||||
<string name="verification_emoji_scissors">Scheren</string>
|
||||
<string name="verification_emoji_lock">sperren</string>
|
||||
<string name="verification_emoji_key">Schlüssel</string>
|
||||
<string name="verification_emoji_hammer">Hammer</string>
|
||||
<string name="verification_emoji_telephone">Telefon</string>
|
||||
<string name="verification_emoji_flag">Flagge</string>
|
||||
<string name="verification_emoji_train">Zug</string>
|
||||
<string name="verification_emoji_bicycle">Fahrrad</string>
|
||||
<string name="verification_emoji_airplane">Flugzeug</string>
|
||||
<string name="verification_emoji_rocket">Rakete</string>
|
||||
<string name="verification_emoji_trophy">Pokal</string>
|
||||
<string name="verification_emoji_ball">Ball</string>
|
||||
<string name="verification_emoji_guitar">Gitarre</string>
|
||||
<string name="verification_emoji_trumpet">Trompete</string>
|
||||
<string name="verification_emoji_bell">Glocke</string>
|
||||
<string name="verification_emoji_anchor">Anker</string>
|
||||
<string name="verification_emoji_headphone">Kopfhörer</string>
|
||||
<string name="verification_emoji_folder">Ordner</string>
|
||||
<string name="verification_emoji_pin">Stecknadel</string>
|
||||
|
||||
<string name="event_status_sending_message">Sende eine Nachricht…</string>
|
||||
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
|
||||
|
||||
</resources>
|
||||
|
@ -167,4 +167,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa:
|
||||
\nKontuaren datuak inportatzen</string>
|
||||
|
||||
<string name="notice_room_update">%s erabiltzaileak gela hau eguneratu du.</string>
|
||||
|
||||
<string name="event_status_sending_message">Mezua bidaltzen…</string>
|
||||
<string name="clear_timeline_send_queue">Garbitu bidalketa-ilara</string>
|
||||
|
||||
</resources>
|
||||
|
@ -168,4 +168,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Alkusynkronointi:
|
||||
\nTuodaan tilin tietoja</string>
|
||||
|
||||
<string name="notice_room_update">%s päivitti tämän huoneen.</string>
|
||||
|
||||
<string name="event_status_sending_message">Lähetetään viestiä…</string>
|
||||
<string name="clear_timeline_send_queue">Tyhjennä lähetysjono</string>
|
||||
|
||||
</resources>
|
||||
|
@ -167,4 +167,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Synchronisation initiale :
|
||||
\nImportation des données du compte</string>
|
||||
|
||||
<string name="notice_room_update">%s a mis à niveau ce salon.</string>
|
||||
|
||||
<string name="event_status_sending_message">Envoi du message…</string>
|
||||
<string name="clear_timeline_send_queue">Vider la file d’envoi</string>
|
||||
|
||||
</resources>
|
||||
|
@ -166,4 +166,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Induló szinkronizáció:
|
||||
\nFiók adatok betöltése</string>
|
||||
|
||||
<string name="notice_room_update">%s frissítette ezt a szobát.</string>
|
||||
|
||||
<string name="event_status_sending_message">Üzenet küldése…</string>
|
||||
<string name="clear_timeline_send_queue">Küldő sor ürítése</string>
|
||||
|
||||
</resources>
|
||||
|
@ -167,4 +167,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Sync iniziale:
|
||||
\nImportazione dati account</string>
|
||||
|
||||
<string name="notice_room_update">%s ha aggiornato questa stanza.</string>
|
||||
|
||||
<string name="event_status_sending_message">Invio messaggio in corso …</string>
|
||||
<string name="clear_timeline_send_queue">Cancella la coda di invio</string>
|
||||
|
||||
</resources>
|
||||
|
@ -1,6 +1,173 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="notice_room_invite_no_invitee">%s\'의 초대</string>
|
||||
<string name="notice_room_invite_no_invitee">%s님의 초대</string>
|
||||
<string name="verification_emoji_headphone">헤드폰</string>
|
||||
<string name="summary_user_sent_image">%1$s님이 사진을 보냈습니다.</string>
|
||||
<string name="summary_user_sent_sticker">%1$s님이 스티커를 보냈습니다.</string>
|
||||
|
||||
<string name="notice_room_invite">%1$s님이 %2$s님을 초대했습니다</string>
|
||||
<string name="notice_room_invite_you">%1$s님이 당신을 초대했습니다</string>
|
||||
<string name="notice_room_join">%1$s님이 참가했습니다</string>
|
||||
<string name="notice_room_leave">%1$s님이 떠났습니다</string>
|
||||
<string name="notice_room_reject">%1$s님이 초대를 거부했습니다</string>
|
||||
<string name="notice_room_kick">%1$s님이 %2$s님을 추방했습니다</string>
|
||||
<string name="notice_room_unban">%1$s님이 %2$s님의 차단을 풀었습니다</string>
|
||||
<string name="notice_room_ban">%1$s님이 %2$s님을 차단했습니다</string>
|
||||
<string name="notice_room_withdraw">%1$s님이 %2$s님의 초대를 취소했습니다</string>
|
||||
<string name="notice_avatar_url_changed">%1$s님이 아바타를 변경했습니다</string>
|
||||
<string name="notice_display_name_set">%1$s님이 표시 이름을 %2$s(으)로 설정했습니다</string>
|
||||
<string name="notice_display_name_changed_from">%1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다</string>
|
||||
<string name="notice_display_name_removed">%1$s님이 표시 이름을 삭제했습니다 (%2$s)</string>
|
||||
<string name="notice_room_topic_changed">%1$s님이 주제를 다음으로 변경했습니다: %2$s</string>
|
||||
<string name="notice_room_name_changed">%1$s님이 방 이름을 다음으로 변경했습니다: %2$s</string>
|
||||
<string name="notice_placed_video_call">%s님이 영상 통화를 걸었습니다.</string>
|
||||
<string name="notice_placed_voice_call">%s님이 음성 통화를 걸었습니다.</string>
|
||||
<string name="notice_answered_call">%s님이 전화를 받았습니다.</string>
|
||||
<string name="notice_ended_call">%s님이 전화를 끊었습니다.</string>
|
||||
<string name="notice_made_future_room_visibility">%1$s님이 이후 %2$s에게 방 기록을 공개했습니다</string>
|
||||
<string name="notice_room_visibility_invited">초대된 시점부터 모든 방 구성원.</string>
|
||||
<string name="notice_room_visibility_joined">들어온 시점부터 모든 방 구성원.</string>
|
||||
<string name="notice_room_visibility_shared">모든 방 구성원.</string>
|
||||
<string name="notice_room_visibility_world_readable">누구나.</string>
|
||||
<string name="notice_room_visibility_unknown">알 수 없음 (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s님이 종단 간 암호화를 켰습니다 (%2$s)</string>
|
||||
<string name="notice_room_update">%s님이 방을 업그레이드했습니다.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s님이 VoIP 회의를 요청했습니다</string>
|
||||
<string name="notice_voip_started">VoIP 회의가 시작했습니다</string>
|
||||
<string name="notice_voip_finished">VoIP 회의가 끝났습니다</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(아바타도 변경됨)</string>
|
||||
<string name="notice_room_name_removed">%1$s님이 방 이름을 삭제했습니다</string>
|
||||
<string name="notice_room_topic_removed">%1$s님이 방 주제를 삭제했습니다</string>
|
||||
<string name="notice_event_redacted">메시지가 삭제되었습니다</string>
|
||||
<string name="notice_event_redacted_by">메시지가 %1$s님에 의해 삭제되었습니다</string>
|
||||
<string name="notice_event_redacted_with_reason">메시지가 삭제되었습니다 [이유: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">메시지가 %1$s님에 의해 삭제되었습니다 [이유: %2$s]</string>
|
||||
<string name="notice_profile_change_redacted">%1$s님이 프로필 %2$s을(를) 업데이트했습니다</string>
|
||||
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">이 답장의 질문</string>
|
||||
|
||||
<string name="could_not_redact">검열할 수 없습니다</string>
|
||||
<string name="unable_to_send_message">메시지를 보낼 수 없습니다</string>
|
||||
|
||||
<string name="message_failed_to_upload">사진 업로드에 실패했습니다</string>
|
||||
|
||||
<string name="network_error">네트워크 오류</string>
|
||||
<string name="matrix_error">Matrix 오류</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">현재 빈 방에 다시 들어갈 수 없습니다.</string>
|
||||
|
||||
<string name="encrypted_message">암호화된 메시지</string>
|
||||
|
||||
<string name="medium_email">이메일 주소</string>
|
||||
<string name="medium_phone_number">전화번호</string>
|
||||
|
||||
<string name="reply_to_an_image">사진을 보냈습니다.</string>
|
||||
<string name="reply_to_a_video">동영상을 보냈습니다.</string>
|
||||
<string name="reply_to_an_audio_file">오디오 파일을 보냈습니다.</string>
|
||||
<string name="reply_to_a_file">파일을 보냈습니다.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">%s에서 초대함</string>
|
||||
<string name="room_displayname_room_invite">방 초대</string>
|
||||
|
||||
<string name="room_displayname_two_members">%1$s님과 %2$s님</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="other">%1$s님 외 %2$d명</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">빈 방</string>
|
||||
|
||||
|
||||
<string name="verification_emoji_dog">개</string>
|
||||
<string name="verification_emoji_cat">고양이</string>
|
||||
<string name="verification_emoji_lion">사자</string>
|
||||
<string name="verification_emoji_horse">말</string>
|
||||
<string name="verification_emoji_unicorn">유니콘</string>
|
||||
<string name="verification_emoji_pig">돼지</string>
|
||||
<string name="verification_emoji_elephant">코끼리</string>
|
||||
<string name="verification_emoji_rabbit">토끼</string>
|
||||
<string name="verification_emoji_panda">판다</string>
|
||||
<string name="verification_emoji_rooster">수탉</string>
|
||||
<string name="verification_emoji_penguin">펭귄</string>
|
||||
<string name="verification_emoji_turtle">거북</string>
|
||||
<string name="verification_emoji_fish">물고기</string>
|
||||
<string name="verification_emoji_octopus">문어</string>
|
||||
<string name="verification_emoji_butterfly">나비</string>
|
||||
<string name="verification_emoji_flower">꽃</string>
|
||||
<string name="verification_emoji_tree">나무</string>
|
||||
<string name="verification_emoji_cactus">선인장</string>
|
||||
<string name="verification_emoji_mushroom">버섯</string>
|
||||
<string name="verification_emoji_globe">지구본</string>
|
||||
<string name="verification_emoji_moon">달</string>
|
||||
<string name="verification_emoji_cloud">구름</string>
|
||||
<string name="verification_emoji_fire">불</string>
|
||||
<string name="verification_emoji_banana">바나나</string>
|
||||
<string name="verification_emoji_apple">사과</string>
|
||||
<string name="verification_emoji_strawberry">딸기</string>
|
||||
<string name="verification_emoji_corn">옥수수</string>
|
||||
<string name="verification_emoji_pizza">피자</string>
|
||||
<string name="verification_emoji_cake">케이크</string>
|
||||
<string name="verification_emoji_heart">하트</string>
|
||||
<string name="verification_emoji_smiley">웃음</string>
|
||||
<string name="verification_emoji_robot">로봇</string>
|
||||
<string name="verification_emoji_hat">모자</string>
|
||||
<string name="verification_emoji_glasses">안경</string>
|
||||
<string name="verification_emoji_wrench">스패너</string>
|
||||
<string name="verification_emoji_santa">산타클로스</string>
|
||||
<string name="verification_emoji_thumbsup">좋아요</string>
|
||||
<string name="verification_emoji_umbrella">우산</string>
|
||||
<string name="verification_emoji_hourglass">모래시계</string>
|
||||
<string name="verification_emoji_clock">시계</string>
|
||||
<string name="verification_emoji_gift">선물</string>
|
||||
<string name="verification_emoji_lightbulb">전구</string>
|
||||
<string name="verification_emoji_book">책</string>
|
||||
<string name="verification_emoji_pencil">연필</string>
|
||||
<string name="verification_emoji_paperclip">클립</string>
|
||||
<string name="verification_emoji_scissors">가위</string>
|
||||
<string name="verification_emoji_lock">자물쇠</string>
|
||||
<string name="verification_emoji_key">열쇠</string>
|
||||
<string name="verification_emoji_hammer">망치</string>
|
||||
<string name="verification_emoji_telephone">전화기</string>
|
||||
<string name="verification_emoji_flag">깃발</string>
|
||||
<string name="verification_emoji_train">기차</string>
|
||||
<string name="verification_emoji_bicycle">자전거</string>
|
||||
<string name="verification_emoji_airplane">비행기</string>
|
||||
<string name="verification_emoji_rocket">로켓</string>
|
||||
<string name="verification_emoji_trophy">트로피</string>
|
||||
<string name="verification_emoji_ball">공</string>
|
||||
<string name="verification_emoji_guitar">기타</string>
|
||||
<string name="verification_emoji_trumpet">트럼펫</string>
|
||||
<string name="verification_emoji_bell">종</string>
|
||||
<string name="verification_emoji_anchor">닻</string>
|
||||
<string name="verification_emoji_folder">폴더</string>
|
||||
<string name="verification_emoji_pin">핀</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">초기 동기화:
|
||||
\n계정 가져오는 중…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">초기 동기화:
|
||||
\n암호 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">초기 동기화:
|
||||
\n방 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">초기 동기화:
|
||||
\n들어간 방 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">초기 동기화:
|
||||
\n초대받은 방 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">초기 동기화:
|
||||
\n떠난 방 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_groups">초기 동기화:
|
||||
\n커뮤니티 가져오는 중</string>
|
||||
<string name="initial_sync_start_importing_account_data">초기 동기화:
|
||||
\n계정 데이터 가져오는 중</string>
|
||||
|
||||
<string name="event_status_sending_message">메시지 보내는 중…</string>
|
||||
<string name="clear_timeline_send_queue">전송 대기 열 지우기</string>
|
||||
|
||||
</resources>
|
||||
|
@ -176,4 +176,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Initiële synchronisatie:
|
||||
\nAccountgegevens worden geïmporteerd</string>
|
||||
|
||||
<string name="notice_room_update">%s heeft dit gesprek opgewaardeerd.</string>
|
||||
|
||||
<string name="event_status_sending_message">Bericht wordt verstuurd…</string>
|
||||
<string name="clear_timeline_send_queue">Uitgaande wachtrij legen</string>
|
||||
|
||||
</resources>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<string name="notice_room_invite">%1$s zaprosił(a) %2$s</string>
|
||||
<string name="notice_room_invite_you">%1$s zaprosił(a) Cię</string>
|
||||
<string name="notice_room_join">%1$s dołączył(a)</string>
|
||||
<string name="notice_room_leave">%1$s wyszedł(-ła)</string>
|
||||
<string name="notice_room_leave">%1$s opuścił(a)</string>
|
||||
<string name="notice_room_reject">%1$s odrzucił(a) zaproszenie</string>
|
||||
<string name="notice_room_kick">%1$s wyrzucił(a) %2$s</string>
|
||||
<string name="notice_room_unban">%1$s odblokował(a) %2$s</string>
|
||||
@ -17,11 +17,11 @@
|
||||
<string name="notice_display_name_changed_from">%1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s</string>
|
||||
<string name="notice_display_name_removed">%1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s)</string>
|
||||
<string name="notice_room_topic_changed">%1$s zmienił(a) temat na: %2$s</string>
|
||||
<string name="unable_to_send_message">Nie udało się wysłać wiadomości</string>
|
||||
<string name="unable_to_send_message">Nie można wysłać wiadomości</string>
|
||||
|
||||
<string name="message_failed_to_upload">Nie udało się wysłać zdjęcia</string>
|
||||
<string name="message_failed_to_upload">Przesyłanie zdjęcia nie powiodło się</string>
|
||||
|
||||
<string name="network_error">ogólne błędy</string>
|
||||
<string name="network_error">Błąd sieci</string>
|
||||
<string name="matrix_error">Błąd Matrixa</string>
|
||||
|
||||
<string name="encrypted_message">Wiadomość zaszyfrowana</string>
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
<string name="notice_room_visibility_shared">wszyscy członkowie pokoju.</string>
|
||||
<string name="notice_room_visibility_world_readable">wszyscy.</string>
|
||||
<string name="notice_room_name_changed">%1$s zmienił(a) znawę pokoju na: %2$s</string>
|
||||
<string name="notice_room_name_changed">%1$s zmienił(a) nazwę pokoju na: %2$s</string>
|
||||
<string name="notice_ended_call">%s zakończył(a) rozmowę.</string>
|
||||
<string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string>
|
||||
<string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string>
|
||||
@ -57,9 +57,9 @@
|
||||
</plurals>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string>
|
||||
<string name="notice_placed_video_call">%s umieścił wideo rozmowe.</string>
|
||||
<string name="notice_placed_voice_call">%s umieścił połączenie głosowe.</string>
|
||||
<string name="notice_made_future_room_visibility">%1$s uczynił historię pokoju widoczną do %2$s</string>
|
||||
<string name="notice_placed_video_call">%s wykonał(a) rozmowę wideo.</string>
|
||||
<string name="notice_placed_voice_call">%s wykonał(a) połączenie głosowe.</string>
|
||||
<string name="notice_made_future_room_visibility">%1$s uczynił(a) przyszłą historię pokoju widoczną dla %2$s</string>
|
||||
<string name="notice_room_visibility_invited">wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni.</string>
|
||||
<string name="notice_room_visibility_joined">wszyscy członkowie pokoju, od momentu w którym dołączyli.</string>
|
||||
<string name="notice_room_visibility_unknown">nieznane (%s).</string>
|
||||
@ -147,4 +147,29 @@
|
||||
<string name="verification_emoji_santa">Mikołaj</string>
|
||||
<string name="verification_emoji_gift">Prezent</string>
|
||||
<string name="verification_emoji_hammer">Młotek</string>
|
||||
<string name="notice_room_update">%s zakutalizował(a) ten pokój.</string>
|
||||
|
||||
<string name="verification_emoji_thumbsup">Kciuk w górę</string>
|
||||
<string name="verification_emoji_lock">Zamek</string>
|
||||
<string name="verification_emoji_ball">Piłka</string>
|
||||
<string name="initial_sync_start_importing_account">Synchronizacja początkowa:
|
||||
\nImportowanie konta…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Synchronizacja początkowa:
|
||||
\nImportowanie kryptografii</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Synchronizacja początkowa:
|
||||
\nImportowanie Pokoi</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Synchronizacja początkowa:
|
||||
\nImportowanie dołączonych Pokoi</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Synchronizacja początkowa:
|
||||
\nImportowanie zaproszonych Pokoi</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Synchronizacja początkowa:
|
||||
\nImportowanie opuszczonych Pokoi</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Synchronizacja początkowa:
|
||||
\nImportowanie Społeczności</string>
|
||||
<string name="initial_sync_start_importing_account_data">Synchronizacja początkowa:
|
||||
\nImportowanie danych Konta</string>
|
||||
|
||||
<string name="event_status_sending_message">Wysyłanie wiadomości…</string>
|
||||
<string name="clear_timeline_send_queue">Wyczyść kolejkę wysyłania</string>
|
||||
|
||||
</resources>
|
||||
|
@ -78,4 +78,10 @@
|
||||
<string name="room_displayname_empty_room">Sala vazia</string>
|
||||
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s enviou um sticker.</string>
|
||||
|
||||
<string name="notice_room_update">%s fez o upgrade da sala.</string>
|
||||
|
||||
<string name="notice_event_redacted">Mensagem removida</string>
|
||||
<string name="notice_event_redacted_by">Mensagem removida por %1$s</string>
|
||||
</resources>
|
||||
|
@ -169,9 +169,9 @@
|
||||
\nИмпорт криптографии</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Начальная синхронизация:
|
||||
\nИмпорт комнат</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Начальная синхронизация:
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Синхронизация начата:
|
||||
\nИмпорт присоединенных комнат</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Начальная синхронизация:
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Синхронизация начата:
|
||||
\nИмпорт приглашенных комнат</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Начальная синхронизация:
|
||||
\nИмпорт покинутых комнат</string>
|
||||
@ -180,4 +180,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">Начальная синхронизация:
|
||||
\nИмпорт данных учетной записи</string>
|
||||
|
||||
<string name="notice_room_update">%s обновил эту комнату.</string>
|
||||
|
||||
<string name="event_status_sending_message">Отправка сообщения…</string>
|
||||
<string name="clear_timeline_send_queue">Очистить очередь отправки</string>
|
||||
|
||||
</resources>
|
||||
|
@ -82,4 +82,95 @@
|
||||
</plurals>
|
||||
|
||||
|
||||
<string name="notice_room_update">%s aktualizoval túto miestnosť.</string>
|
||||
|
||||
<string name="notice_event_redacted">Správa odstránená</string>
|
||||
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
|
||||
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
|
||||
<string name="verification_emoji_dog">Pes</string>
|
||||
<string name="verification_emoji_cat">Mačka</string>
|
||||
<string name="verification_emoji_lion">Lev</string>
|
||||
<string name="verification_emoji_horse">Kôň</string>
|
||||
<string name="verification_emoji_unicorn">Jednorožec</string>
|
||||
<string name="verification_emoji_pig">Prasa</string>
|
||||
<string name="verification_emoji_elephant">Slon</string>
|
||||
<string name="verification_emoji_rabbit">Zajac</string>
|
||||
<string name="verification_emoji_panda">Panda</string>
|
||||
<string name="verification_emoji_rooster">Kohút</string>
|
||||
<string name="verification_emoji_penguin">Tučniak</string>
|
||||
<string name="verification_emoji_turtle">Korytnačka</string>
|
||||
<string name="verification_emoji_fish">Ryba</string>
|
||||
<string name="verification_emoji_octopus">Chobotnica</string>
|
||||
<string name="verification_emoji_butterfly">Motýľ</string>
|
||||
<string name="verification_emoji_flower">Kvetina</string>
|
||||
<string name="verification_emoji_tree">Strom</string>
|
||||
<string name="verification_emoji_cactus">Kaktus</string>
|
||||
<string name="verification_emoji_mushroom">Hríb</string>
|
||||
<string name="verification_emoji_globe">Zemeguľa</string>
|
||||
<string name="verification_emoji_moon">Mesiac</string>
|
||||
<string name="verification_emoji_cloud">Oblak</string>
|
||||
<string name="verification_emoji_fire">Oheň</string>
|
||||
<string name="verification_emoji_banana">Banán</string>
|
||||
<string name="verification_emoji_apple">Jablko</string>
|
||||
<string name="verification_emoji_strawberry">Jahoda</string>
|
||||
<string name="verification_emoji_corn">Kukurica</string>
|
||||
<string name="verification_emoji_pizza">Pizza</string>
|
||||
<string name="verification_emoji_cake">Koláč</string>
|
||||
<string name="verification_emoji_heart">Srdce</string>
|
||||
<string name="verification_emoji_smiley">Úsmev</string>
|
||||
<string name="verification_emoji_robot">Robot</string>
|
||||
<string name="verification_emoji_hat">Klobúk</string>
|
||||
<string name="verification_emoji_glasses">Okuliare</string>
|
||||
<string name="verification_emoji_wrench">Skrutkovač</string>
|
||||
<string name="verification_emoji_santa">Mikuláš</string>
|
||||
<string name="verification_emoji_thumbsup">Palec nahor</string>
|
||||
<string name="verification_emoji_umbrella">Dáždnik</string>
|
||||
<string name="verification_emoji_hourglass">Presýpacie hodiny</string>
|
||||
<string name="verification_emoji_clock">Hodiny</string>
|
||||
<string name="verification_emoji_gift">Darček</string>
|
||||
<string name="verification_emoji_lightbulb">Žiarovka</string>
|
||||
<string name="verification_emoji_book">Kniha</string>
|
||||
<string name="verification_emoji_pencil">Ceruzka</string>
|
||||
<string name="verification_emoji_paperclip">Kancelárska sponka</string>
|
||||
<string name="verification_emoji_scissors">Nožnice</string>
|
||||
<string name="verification_emoji_lock">Zámok</string>
|
||||
<string name="verification_emoji_key">Kľúč</string>
|
||||
<string name="verification_emoji_hammer">Kladivo</string>
|
||||
<string name="verification_emoji_telephone">Telefón</string>
|
||||
<string name="verification_emoji_flag">Vlajka</string>
|
||||
<string name="verification_emoji_train">Vlak</string>
|
||||
<string name="verification_emoji_bicycle">Bicykel</string>
|
||||
<string name="verification_emoji_airplane">Lietadlo</string>
|
||||
<string name="verification_emoji_rocket">Raketa</string>
|
||||
<string name="verification_emoji_trophy">Trofej</string>
|
||||
<string name="verification_emoji_ball">Lopta</string>
|
||||
<string name="verification_emoji_guitar">Gitara</string>
|
||||
<string name="verification_emoji_trumpet">Trúbka</string>
|
||||
<string name="verification_emoji_bell">Zvonček</string>
|
||||
<string name="verification_emoji_anchor">Kotva</string>
|
||||
<string name="verification_emoji_headphone">Schlúchadlá</string>
|
||||
<string name="verification_emoji_folder">Priečinok</string>
|
||||
<string name="verification_emoji_pin">Pin</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Úvodná synchronizácia:
|
||||
\nPrebieha import účtu…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Úvodná synchronizácia:
|
||||
\nPrebieha import šifrovacích kľúčov</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Úvodná synchronizácia:
|
||||
\nPrebieha import miestností</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Úvodná synchronizácia:
|
||||
\nPrebieha import miestností, do ktorých ste vstúpili</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Úvodná synchronizácia:
|
||||
\nPrebieha import pozvánok</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Úvodná synchronizácia:
|
||||
\nPrebieha import opustených miestností</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Úvodná synchronizácia:
|
||||
\nPrebieha import komunít</string>
|
||||
<string name="initial_sync_start_importing_account_data">Úvodná synchronizácia:
|
||||
\nPrebieha import údajov účtu</string>
|
||||
|
||||
<string name="event_status_sending_message">Odosielanie správy…</string>
|
||||
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>
|
||||
|
||||
</resources>
|
||||
|
@ -146,4 +146,26 @@
|
||||
<string name="verification_emoji_anchor">Spirancë</string>
|
||||
<string name="verification_emoji_headphone">Kufje</string>
|
||||
<string name="verification_emoji_folder">Dosje</string>
|
||||
<string name="notice_room_update">%s e përmirësoi këtë dhomë.</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
|
||||
\nPo importohet llogaria…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
|
||||
\nPo importohet kriptografi</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Njëkohësimi Fillestar:
|
||||
\nPo importohen Dhoma</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Njëkohësimi Fillestar:
|
||||
\nPo importohen Dhoma Ku Është Bërë Hyrje</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Njëkohësimi Fillestar:
|
||||
\nPo importohen Dhoma Me Ftesë</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Njëkohësimi Fillestar:
|
||||
\nPo importohen Dhoma të Braktisura</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Njëkohësimi Fillestar:
|
||||
\nPo importohen Bashkësi</string>
|
||||
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
|
||||
\nPo importohet të Dhëna Llogarie</string>
|
||||
|
||||
<string name="event_status_sending_message">Po dërgohet mesazh…</string>
|
||||
<string name="clear_timeline_send_queue">Spastro radhë pritjeje</string>
|
||||
|
||||
</resources>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<string name="notice_room_third_party_registered_invite">%1$s èt d’uutnodigienge vo %2$s anveird</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">’t Toestel van den afzender èt geen sleutels vo dit bericht gesteurd.</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">’t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Als antwoord ip</string>
|
||||
|
||||
@ -150,21 +150,26 @@
|
||||
<string name="verification_emoji_folder">Mappe</string>
|
||||
<string name="verification_emoji_pin">Pinne</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account">Initiële synchronisoasje:
|
||||
\nAccount wor geïmporteerd…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_crypto">Initiële synchronisoasje:
|
||||
\nCrypto wor geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_rooms">Initiële synchronisoasje:
|
||||
\nGesprekkn wordn geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoasje:
|
||||
\nDeelgenoomn gesprekken wordn geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoasje:
|
||||
\nUutgenodigde gesprekkn wordn geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoasje:
|
||||
\nVerloatn gesprekkn wordn geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_groups">Initiële synchronisoasje:
|
||||
\nGemeenschappn wordn geïmporteerd</string>
|
||||
<string name="initial_sync_start_importing_account_data">Initiële synchronisoatie:
|
||||
<string name="initial_sync_start_importing_account_data">Initiële synchronisoasje:
|
||||
\nAccountgegeevns wordn geïmporteerd</string>
|
||||
|
||||
<string name="notice_room_update">%s èt da gesprek hier ipgewoardeerd.</string>
|
||||
|
||||
<string name="event_status_sending_message">Bericht wor verstuurd…</string>
|
||||
<string name="clear_timeline_send_queue">Uutgoande wachtreeke leegn</string>
|
||||
|
||||
</resources>
|
||||
|
@ -162,4 +162,9 @@
|
||||
<string name="initial_sync_start_importing_account_data">初始化同步:
|
||||
\n正在导入账号数据</string>
|
||||
|
||||
<string name="notice_room_update">%s 升级了聊天室。</string>
|
||||
|
||||
<string name="event_status_sending_message">正在发送消息…</string>
|
||||
<string name="clear_timeline_send_queue">清除正在发送队列</string>
|
||||
|
||||
</resources>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user