Compare commits

..

70 Commits

Author SHA1 Message Date
07fca0922b Merge branch 'release/0.5.0' 2019-09-17 14:50:55 +02:00
ba9d119892 Prepare release 0.5.0 2019-09-17 14:50:43 +02:00
4453f0ced9 Merge pull request #560 from vector-im/feature/no_network
Display a "No network" banner when the device has no network
2019-09-17 14:40:42 +02:00
77168bfd6a Merge pull request #558 from vector-im/feature/login_sso
Quick implementation of SSO login - Also handling of magic link
2019-09-17 14:28:04 +02:00
25e9a179d2 SyncThread: Fix issue when network is back and the app was in background: do not restart the thread 2019-09-17 14:26:30 +02:00
73ec0f5a83 NetworkConnectivityChecker: filter onConnected callbacks (several callback if Wifi and LTE is connected)
Also do not use merlinsBeard.isConnected, which return trus even if there is no internet access (ex: with Wifi hotspot)
2019-09-17 14:22:08 +02:00
993fa74252 Cleanup after BillCarsonFr's review 2019-09-17 11:24:37 +02:00
38fc4984fe Display a no network indicator when there is no network: Create a dedicated View 2019-09-17 11:13:00 +02:00
695d8cce00 Display a no network indicator when there is no network (#559) 2019-09-17 10:59:58 +02:00
07e99901e1 SecretStoringUtils -> move to internal package 2019-09-17 10:38:37 +02:00
20f53e9a58 Signout: propose the user to retry in case of error 2019-09-17 10:33:27 +02:00
ced72aff4f Revert change done to save alias for the client 2019-09-17 10:32:09 +02:00
fdaaca49c2 Code quality (bad import) 2019-09-16 19:27:13 +02:00
3485f023b0 All current notifications were dismissed by mistake when the app is launched from the launcher 2019-09-16 19:24:52 +02:00
384dd100e9 Daggerization and Kotlinification of SecretStoringUtils 2019-09-16 19:19:14 +02:00
1ba8a58219 Cleanup SecretStoringUtils, and delete keys when user signs out 2019-09-16 18:29:06 +02:00
c8010561fc Rework on sign out task 2019-09-16 17:45:26 +02:00
1f127335bc Daggerization of RealmKeysUtils 2019-09-16 15:50:56 +02:00
138a210a73 Dagger: Screen component now exposes ActiveSessionHolder instead of Session 2019-09-16 14:43:39 +02:00
ca6bcde82d Re add the remove CurlLoggingInterceptor 2019-09-16 14:43:08 +02:00
6bda437f5d Auto configure homeserver and identity server URLs of LoginActivity with a magic link 2019-09-16 10:58:51 +02:00
3e6b65e174 Handle M_CONSENT_NOT_GIVEN error (#64) 2019-09-13 18:21:56 +02:00
137dcab734 Curl login interceptor now log the AT (on debug mode) 2019-09-13 16:20:19 +02:00
b22b8fba02 Fix the mess up with OnBackPress support on Fragment 2019-09-13 15:55:33 +02:00
3ccdf4a244 Login: some cleanup 2019-09-13 15:35:44 +02:00
5fbd271b1c Login: add SSO support 2019-09-13 15:19:45 +02:00
db8ea0f5e8 Login: check login flow - step 1 2019-09-13 11:08:54 +02:00
a47a3ead1f Login: move login code to the ViewModel 2019-09-13 10:39:22 +02:00
05b2092ffc Login: move existing code to a Fragment, MvRx style 2019-09-13 10:07:55 +02:00
6249a59203 Merge pull request #554 from vector-im/feature/build_number
Fix issue with bad versionCode generated by Buildkite (#553)
2019-09-12 17:24:46 +02:00
618e9a4f52 Fix issue with bad versionCode generated by Buildkite (#553) 2019-09-12 16:17:44 +02:00
f2c8d4ad02 Merge pull request #549 from vector-im/feature/third_party_invite
Fix rendering issue of accepted third party invitation event
2019-09-06 16:36:30 +02:00
be524472ec Merge pull request #546 from vector-im/feature/cleanup
Cleanup
2019-09-06 16:25:08 +02:00
1b82a1a24d Cleanup 2019-09-06 15:52:29 +02:00
cf0b331c3b Handle invite to the current user rendering 2019-09-06 15:48:42 +02:00
2a92a3dc80 Fix rendering issue of accepted third party invitation event 2019-09-06 14:34:52 +02:00
012840abba Progress in initial sync dialog is decreasing for a step and should not (#532) 2019-09-05 18:14:05 +02:00
a5975a099e Cleanup and document DefaultInitialSyncProgressService 2019-09-05 17:23:09 +02:00
38da4b9ee5 Cleanup and document DefaultInitialSyncProgressService 2019-09-05 17:02:03 +02:00
242e60fcaa Rename CryptoManager to DefaultCryptoService 2019-09-05 16:14:34 +02:00
a23be05cbf Better type 2019-09-05 16:04:41 +02:00
ed39b02924 Avoid using keyword for variable names 2019-09-05 16:04:41 +02:00
fe931b5361 Merge pull request #418 from Dominaezzz/kotlinify-1
Some more kotlinification
2019-09-05 16:02:30 +02:00
90d9cd0587 Merge pull request #416 from Dominaezzz/kt-remove_java_util
Remove most usages of the java.util package
2019-09-05 15:33:03 +02:00
9cedb18921 Merge pull request #538 from vector-im/feature/log_mgmt
Reduce release build log level
2019-09-05 15:24:04 +02:00
e89ba7b87b Update wording 2019-09-05 15:23:38 +02:00
902657c22a Merge pull request #537 from vector-im/feature/fix_crash
Fix crash due to missing informationData (#535)
2019-09-02 15:31:28 +02:00
eec2abf164 Reduce release build log level 2019-09-02 14:33:53 +02:00
6879cc8ca8 Fix crash due to missing informationData (#535) 2019-09-02 14:24:36 +02:00
fd6bbbd3b5 Fix issue with version name (Fixes #533) 2019-08-30 15:57:39 +02:00
0ff0b014a9 Version++ (0.5.0) 2019-08-30 15:07:04 +02:00
a89f0ddd1d Merge branch 'release/0.4.0' 2019-08-30 15:04:43 +02:00
fdc9e84dd5 Merge branch 'release/0.4.0' into develop 2019-08-30 15:04:43 +02:00
58f878fca9 Prepare version 0.4.0 2019-08-30 15:04:28 +02:00
88095e4bd9 Add entry in change file 2019-08-30 14:54:15 +02:00
47d22a3d5e Import translation from Riot and MatrixSDK 2019-08-30 11:21:43 +02:00
28e82cb8ea Merge pull request #531 from vector-im/feature/fix_crash_530
Fix / EmojiCompat not initialized
2019-08-29 17:46:51 +02:00
35817245cb refactoring, code review 2019-08-29 17:27:49 +02:00
75266f42bb Fix / EmojiCompat not initialized 2019-08-29 16:49:22 +02:00
95c4c9ce56 Merge pull request #527 from vector-im/feature/privacy
Privacy: remove log of notifiable event (#519)
2019-08-29 12:16:34 +02:00
ce5570105d Privacy: remove log of notifiable event (#519) 2019-08-29 10:36:45 +02:00
188a9aebfa Merge pull request #525 from vector-im/feature/read_receipt_cleanup
Feature/read receipt cleanup
2019-08-29 10:19:06 +02:00
c95223f5d2 Add long click support on unsupported event 2019-08-28 18:17:37 +02:00
ef0362ba9c Display Read Receipt on unsupported events 2019-08-28 17:31:31 +02:00
ea242f6737 Hide ReadReceipt View when it is not relevant 2019-08-28 17:17:37 +02:00
9cd69d1e33 Merge branch 'release/0.3.0' 2019-08-08 16:45:03 +02:00
456908c851 Merge branch 'develop' into kt-remove_java_util 2019-08-06 18:27:39 +01:00
215324a03e Some kotlinification
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-08-06 11:36:39 +01:00
02e342849f Remove most usages of the java.util package
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-21 23:23:56 +01:00
df6080b1da Merge branch 'release/0.2.0' 2019-07-18 17:47:39 +02:00
178 changed files with 5439 additions and 1082 deletions

View File

@ -1,4 +1,25 @@
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:
@ -7,21 +28,14 @@ Features:
Improvements:
- Reactions: Reinstate the ability to react with non-unicode keys (#307)
Other changes:
-
Bugfix:
- 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
Translations:
-
Build:
-
- Privacy: remove log of notifiable event (#519)
- Fix crash with EmojiCompat (#530)
Changes in RiotX 0.3.0 (2019-08-08)
===================================================

View File

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

View File

@ -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,

View File

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

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.comparators
import im.vector.matrix.android.api.interfaces.DatedObject
import java.util.*
object DatedObjectComparators {

View File

@ -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

View File

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

View File

@ -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)
}

View File

@ -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
)
}

View File

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

View File

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

View File

@ -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 {

View File

@ -29,11 +29,10 @@ data class RoomSummary(
val topic: String = "",
val avatarUrl: String = "",
val isDirect: Boolean = false,
val latestPreviewableEvent: TimelineEvent? = null,
val latestEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(),
val notificationCount: Int = 0,
val highlightCount: Int = 0,
val hasUnreadMessages: Boolean = false,
val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,11 +62,9 @@ import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
@ -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 + ")"
}
}

View File

@ -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,

View File

@ -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

View File

@ -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("=", "")
}
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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(

View File

@ -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)
}
/**

View File

@ -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)
}
}

View File

@ -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
)
}

View File

@ -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())
}
}

View File

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

View File

@ -20,10 +20,9 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
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.api.session.room.read.ReadService
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(
@ -31,12 +30,12 @@ internal class RoomSummaryMapper @Inject constructor(
val timelineEventMapper: TimelineEventMapper
) {
fun map(roomSummaryEntity: RoomSummaryEntity, readService: ReadService?): RoomSummary {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map {
RoomTag(it.tagName, it.tagOrder)
}
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
val latestEvent = roomSummaryEntity.latestEvent?.let {
timelineEventMapper.map(it)
}
if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
@ -44,30 +43,26 @@ 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) {
}
}
val hasUnreadMessages = roomSummaryEntity.notificationCount > 0
//avoid this call if we are sure there are unread events
|| latestEvent?.root?.eventId?.let { readService?.isEventRead(it)?.not() } ?: false
return RoomSummary(
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",
topic = roomSummaryEntity.topic ?: "",
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
isDirect = roomSummaryEntity.isDirect,
latestPreviewableEvent = latestEvent,
latestEvent = latestEvent,
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount,
hasUnreadMessages = hasUnreadMessages,
tags = tags,
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState

View File

@ -26,7 +26,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var avatarUrl: String? = "",
var topic: String? = "",
var latestPreviewableEvent: TimelineEventEntity? = null,
var latestEvent: TimelineEventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,

View File

@ -0,0 +1,23 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UserCacheDirectory

View File

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

View File

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

View File

@ -0,0 +1,23 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UserMd5

View File

@ -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() {

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -35,16 +35,15 @@ import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.util.MatrixCallbackDelegate
import im.vector.matrix.android.internal.crypto.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()

View File

@ -27,21 +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.DefaultRoomFactory
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.RoomFactory
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
@ -53,6 +52,7 @@ internal abstract class SessionModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "session_db_"
@JvmStatic
@Provides
@ -67,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()
@ -101,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()
}
@ -142,4 +168,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
}

View File

@ -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) }
}

View File

@ -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

View File

@ -58,7 +58,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
}
return Transformations.map(liveRealmData) { results ->
val roomSummaries = results.map { roomSummaryMapper.map(it, this) }
val roomSummaries = results.map { roomSummaryMapper.map(it) }
if (roomSummaries.isEmpty()) {
// Create a dummy RoomSummary to avoid Crash during Sign Out or clear cache
@ -72,7 +72,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
override fun roomSummary(): RoomSummary? {
return monarchy.fetchAllMappedSync(
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ roomSummaryMapper.map(it, this) }
{ roomSummaryMapper.map(it) }
).firstOrNull()
}

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
@ -70,7 +69,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
},
{ roomSummaryMapper.map(it, getRoom(it.roomId)) }
{ roomSummaryMapper.map(it) }
)
}

View File

@ -85,7 +85,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.membership = membership
}
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
val otherRoomMembers = RoomMembers(realm, roomId)
@ -98,7 +98,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.latestEvent = latestEvent
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)

View File

@ -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()
}
/**

View File

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

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.util
package im.vector.matrix.android.internal.session.securestorage
import android.content.Context
import android.os.Build
@ -22,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)
}
}

View File

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

View File

@ -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)
}

View File

@ -23,7 +23,7 @@ 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.internal.crypto.CryptoManager
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
@ -50,7 +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 cryptoManager: CryptoManager,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService,
private val processForPushTask: ProcessEventForPushTask,
@ -97,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)
}
}
@ -113,20 +113,19 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync,
isInitialSync: Boolean): RoomEntity {
isInitalSync: Boolean): RoomEntity {
Timber.v("Handle join sync for room $roomId")
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync)
}
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
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()
@ -135,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)
}
@ -167,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)
@ -181,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()
@ -214,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)

View File

@ -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)

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -19,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 {

View File

@ -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

View File

@ -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()

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 denvoi</string>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -48,7 +48,7 @@
<string name="notice_room_third_party_registered_invite">%1$s èt duutnodigienge 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>

View File

@ -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>

View File

@ -165,4 +165,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>

View File

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

View File

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

View File

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

View File

@ -65,6 +65,19 @@
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" />
<activity android:name=".features.link.LinkHandlerActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="riot.im" />
<data android:pathPrefix="/config/" />
</intent-filter>
</activity>
<!-- Services -->

View File

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

View File

@ -0,0 +1,67 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EmojiCompatWrapper @Inject constructor(private val context: Context) {
private var initialized = false
fun init(fontRequest: FontRequest) {
//Use emoji compat for the benefit of emoji spans
val config = FontRequestEmojiCompatConfig(context, fontRequest)
// we want to replace all emojis with selected font
.setReplaceAll(true)
//Debug options
// .setEmojiSpanIndicatorEnabled(true)
// .setEmojiSpanIndicatorColor(Color.GREEN)
EmojiCompat.init(config)
.registerInitCallback(object : EmojiCompat.InitCallback() {
override fun onInitialized() {
Timber.v("Emoji compat onInitialized success ")
initialized = true
}
override fun onFailed(throwable: Throwable?) {
Timber.e(throwable, "Failed to init EmojiCompat")
}
})
}
fun safeEmojiSpanify(sequence: CharSequence): CharSequence {
if (initialized) {
try {
return EmojiCompat.get().process(sequence)
} catch (throwable: Throwable) {
//Defensive coding against error (should not happend as it is initialized)
Timber.e(throwable, "Failed to init EmojiCompat")
return sequence
}
} else {
return sequence
}
}
}

View File

@ -19,13 +19,10 @@ package im.vector.riotx
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.os.Handler
import android.os.HandlerThread
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
@ -69,6 +66,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@ -88,9 +86,12 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this)
// Log
VectorFileLogger.init(this)
Timber.plant(Timber.DebugTree(), VectorFileLogger)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
Timber.plant(vectorComponent.vectorFileLogger())
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}
@ -109,21 +110,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
vectorConfiguration.initConfiguration()
//Use emoji compat for the benefit of emoji spans
val config = FontRequestEmojiCompatConfig(this, fontRequest)
.setReplaceAll(true) // we want to replace all emojis with selected font
// .setEmojiSpanIndicatorEnabled(true)
// .setEmojiSpanIndicatorColor(Color.GREEN)
EmojiCompat.init(config)
.registerInitCallback(object : EmojiCompat.InitCallback() {
override fun onInitialized() {
Timber.v("Emoji compat onInitialized success ")
}
override fun onFailed(throwable: Throwable?) {
Timber.e(throwable,"Failed to init EmojiCompat")
}
})
emojiCompatWrapper.init(fontRequest)
NotificationUtils.createNotificationChannels(applicationContext)
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {

View File

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

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.configuration.VectorConfiguration
@ -40,6 +41,7 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Singleton
@ -70,6 +72,8 @@ interface VectorComponent {
fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper() : EmojiCompatWrapper
fun eventHtmlRenderer(): EventHtmlRenderer
fun navigator(): Navigator
@ -98,6 +102,8 @@ interface VectorComponent {
fun vectorPreferences(): VectorPreferences
fun vectorFileLogger(): VectorFileLogger
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): VectorComponent

View File

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

View File

@ -17,6 +17,7 @@
package im.vector.riotx.core.error
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import javax.inject.Inject
@ -34,8 +35,13 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
null -> null
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
is Failure.ServerError -> {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }
if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) {
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
} else {
throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() }
}
}
else -> throwable.localizedMessage
}

View File

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

View File

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

View File

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

View File

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

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