Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
oogm 2021-03-26 14:09:21 +01:00
commit 7453509df4
97 changed files with 1574 additions and 622 deletions

View file

@ -7,27 +7,27 @@ assignees: ''
---
**Describe the bug**
#### Describe the bug
A clear and concise description of what the bug is.
**To Reproduce**
#### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
#### Expected behavior
A clear and concise description of what you expected to happen.
**Screenshots**
#### Screenshots
If applicable, add screenshots to help explain your problem.
**Smartphone (please complete the following information):**
#### Smartphone (please complete the following information):
- Device: [e.g. Samsung S6]
- OS: [e.g. Android 6.0]
**Additional context**
#### Additional context
- App version and store [e.g. 1.0.0 - F-Droid]
- Homeserver: [e.g. matrix.org]

View file

@ -7,14 +7,14 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
#### Describe the solution you'd like.
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
#### Describe alternatives you've considered.
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
#### Additional context
Add any other context or screenshots about the feature request here.

View file

@ -1,14 +1,21 @@
Changes in Element 1.1.2 (2021-XX-XX)
Changes in Element 1.1.4 (2021-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
-
- Split network request `/keys/query` into smaller requests (250 users max) (#2925)
- Crypto improvement | Bulk send NO_OLM withheld code
- Display the room shield in all room setting screens
- Improve message with Emoji only detection (#3017)
- Picture preview when replying. Also add the image preview in the message detail bottomsheet (#2916)
- Api interceptor to allow app developers peek responses (#2986)
Bugfix 🐛:
-
- Fix bad theme change for the MainActivity
- Handle encrypted reactions (#2509)
- Disable URL preview for some domains (#2995)
Translations 🗣:
-
@ -23,13 +30,33 @@ Test:
-
Other changes:
-
- Add version details on the login screen, in debug or developer mode
Changes in Element 1.1.1 (2021-XX-XX)
Changes in Element 1.1.3 (2021-03-18)
===================================================
Features ✨:
-
Bugfix 🐛:
- Fix regression in UpdateTrustWorker (introduced in 1.1.2)
- Timeline : Fix ripple effect on text item and fix background color of separators.
Changes in Element 1.1.2 (2021-03-16) (was not published tp GPlay prod)
===================================================
Improvements 🙌:
- Lazy storage of ReadReceipts
- Do not load room members in e2e after init sync
Bugfix 🐛:
- Add option to cancel stuck messages at bottom of timeline see #516
- Ensure message are decrypted in the room list after a clear cache
- Regression: Video will not play upon tap, but only after swipe #2928
- Cross signing now works with servers with an explicit port in the servername
Other changes:
- Change formatting on issue templates to proper headings.
Changes in Element 1.1.1 (2021-03-10) (was not published tp GPlay prod)
===================================================
Improvements 🙌:
- Allow non-HTTPS connections to homeservers on Tor (#2941)
@ -52,16 +79,10 @@ Bugfix 🐛:
Translations 🗣:
- All string resources and translations have been moved to the application module. Weblate project for the SDK will be removed.
SDK API changes ⚠️:
-
Build 🧱:
- Update a lot of dependencies, with the help of dependabot.
- Add a script to download and install APK from the CI
Test:
-
Other changes:
- Rework edition of event management

View file

@ -69,7 +69,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.0-beta02"
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation 'com.google.android.material:material:1.3.0'
}

View file

@ -2,7 +2,7 @@
buildscript {
// Ref: https://kotlinlang.org/releases.html
ext.kotlin_version = '1.4.31'
ext.kotlin_version = '1.4.32'
ext.kotlin_coroutines_version = "1.4.2"
repositories {
google()
@ -12,7 +12,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.google.gms:google-services:4.3.5'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1'

View file

@ -2,7 +2,7 @@ This document aims to describe how Element android displays notifications to the
# Table of Contents
1. [Prerequisites Knowledge](#prerequisites-knowledge)
* [How does a matrix client gets a message from a Home Server?](#how-does-a-matrix-client-gets-a-message-from-a-home-server)
* [How does a matrix client get a message from a Home Server?](#how-does-a-matrix-client-get-a-message-from-a-home-server)
* [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification)
* [Push VS Notification](#push-vs-notification)
* [Push in the matrix federated world](#push-in-the-matrix-federated-world)
@ -22,7 +22,7 @@ First let's start with some prerequisite knowledge
# Prerequisites Knowledge
## How does a matrix client gets a message from a Home Server?
## How does a matrix client get a message from a Home Server?
In order to get messages from a home server, a matrix client need to perform a ``sync`` operation.

View file

@ -0,0 +1,2 @@
Main changes in this version: performance improvement and bug fixes!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.2

View file

@ -0,0 +1,2 @@
Main changes in this version: performance improvement and bug fixes!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.3

View file

@ -166,13 +166,13 @@ dependencies {
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.19'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.20'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1'
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation 'io.mockk:mockk:1.10.6'
testImplementation 'io.mockk:mockk:1.11.0'
testImplementation 'org.amshove.kluent:kluent-android:1.65'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
// Plant Timber tree for test
@ -185,8 +185,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.61'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
androidTestImplementation 'io.mockk:mockk-android:1.10.6'
androidTestImplementation 'io.mockk:mockk-android:1.11.0'
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
// Plant Timber tree for test

View file

@ -20,7 +20,6 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.matrix.android.sdk.test.shared.createTimberTestRule
import org.junit.Rule
import java.io.File
interface InstrumentedTest {
@ -30,8 +29,4 @@ interface InstrumentedTest {
fun context(): Context {
return ApplicationProvider.getApplicationContext()
}
fun cacheDir(): File {
return context().cacheDir
}
}

View file

@ -27,9 +27,12 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.network.ApiInterceptorListener
import org.matrix.android.sdk.api.network.ApiPath
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.network.ApiInterceptor
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
@ -51,6 +54,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
@Inject internal lateinit var apiInterceptor: ApiInterceptor
private val uiHandler = Handler(Looper.getMainLooper())
@ -79,6 +83,14 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return legacySessionImporter
}
fun registerApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) {
apiInterceptor.addListener(path, listener)
}
fun unregisterApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) {
apiInterceptor.removeListener(path, listener)
}
companion object {
private lateinit var instance: Matrix

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.network
import org.amshove.kluent.shouldBeEqualTo
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import timber.log.Timber
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class ApiInterceptorTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
fun apiInterceptorTest() {
val responses = mutableListOf<String>()
val listener = object : ApiInterceptorListener {
override fun onApiResponse(path: ApiPath, response: String) {
Timber.w("onApiResponse($path): $response")
responses.add(response)
}
}
commonTestHelper.matrix.registerApiInterceptorListener(ApiPath.REGISTER, listener)
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
commonTestHelper.signOutAndClose(session)
commonTestHelper.matrix.unregisterApiInterceptorListener(ApiPath.REGISTER, listener)
responses.size shouldBeEqualTo 2
}
}

View file

@ -25,9 +25,12 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.network.ApiInterceptorListener
import org.matrix.android.sdk.api.network.ApiPath
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.ApiInterceptor
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
@ -49,6 +52,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
@Inject internal lateinit var apiInterceptor: ApiInterceptor
init {
Monarchy.init(context)
@ -73,6 +77,14 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return legacySessionImporter
}
fun registerApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) {
apiInterceptor.addListener(path, listener)
}
fun unregisterApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) {
apiInterceptor.removeListener(path, listener)
}
companion object {
private lateinit var instance: Matrix

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.network
interface ApiInterceptorListener {
fun onApiResponse(path: ApiPath, response: String)
}

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.network
import org.matrix.android.sdk.internal.network.NetworkConstants
enum class ApiPath(val path: String, val method: String) {
// AuthApi
VERSIONS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "versions", "GET"),
REGISTER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register", "POST"),
ADD_3PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken", "POST"),
LOGIN_FLOWS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "GET"),
LOGIN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "POST"),
RESET_PASSWORD(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken", "POST"),
RESET_PASSWORD_MAIL_CONFIRMED(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password", "POST"),
// DirectoryApi
ROOM_ID_BY_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "GET"),
ROOM_DIRECTORY_VISIBILITY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}", "GET"),
SET_ROOM_DIRECTORY_VISIBILITY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}", "PUT"),
ADD_ROOM_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "PUT"),
DELETE_ROOM_ALIAS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}", "DELETE"),
// CryptoApi
GET_DEVICES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices", "GET"),
GET_DEVICE_INFO(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}", "GET"),
UPLOAD_KEYS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload", "POST"),
DOWNLOAD_KEYS_FOR_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query", "POST"),
UPLOAD_SIGNING_KEYS(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload", "POST"),
UPLOAD_SIGNATURES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload", "POST"),
CLAIM_ONE_TIME_KEYS_FOR_USERS_DEVICES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim", "POST"),
SEND_TO_DEVICE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}", "PUT"),
DELETE_DEVICE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", "DELETE"),
UPDATE_DEVICE_INFO(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", "PUT"),
GET_KEY_CHANGES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes", "GET"),
// RoomKeysApi
CREATE_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version", "POST"),
GET_KEYS_BACKUP_LAST_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version", "GET"),
GET_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "GET"),
UPDATE_KEYS_BACKUP_VERSION(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "PUT"),
STORE_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "PUT"),
STORE_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "PUT"),
STORE_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "PUT"),
GET_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "GET"),
GET_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "GET"),
GET_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "GET"),
DELETE_ROOM_SESSION_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}", "DELETE"),
DELETE_ROOM_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}", "DELETE"),
DELETE_SESSIONS_DATA(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys", "DELETE"),
DELETE_BACKUP(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}", "DELETE"),
// AccountApi
CHANGE_PASSWORD(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password", "POST"),
DEACTIVATE_ACCOUNT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate", "POST"),
// SearchApi
SEARCH(NetworkConstants.URI_API_PREFIX_PATH_R0 + "search", "POST"),
// FederationApi
GET_FEDERATION_VERSION(NetworkConstants.URI_FEDERATION_PATH + "version", "GET"),
// VoipApi
GET_TURN_SERVER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer", "GET"),
// PushGatewayApi
NOTIFY_PUSH_GATEWAY(NetworkConstants.URI_PUSH_GATEWAY_PREFIX_PATH + "notify", "POST"),
// GroupApi
GET_GROUP_SUMMARY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary", "GET"),
GET_GROUP_ROOMS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms", "GET"),
GET_GROUP_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users", "GET"),
// CapabilitiesApi
GET_CAPABILITIES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities", "GET"),
GET_VERSIONS(NetworkConstants.URI_API_PREFIX_PATH_ + "versions", "GET"),
PING(NetworkConstants.URI_API_PREFIX_PATH_ + "versions", "GET"),
// IdentityApi
GET_ACCOUNT(NetworkConstants.URI_IDENTITY_PATH_V2 + "account", "GET"),
LOGOUT(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout", "POST"),
IDENTITY_HAS_DETAILS(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details", "GET"),
LOOKUP(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup", "POST"),
REQUEST_TOKEN_TO_BIND_EMAIL(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken", "POST"),
REQUEST_TOKEN_TO_BIND_MSISDN(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken", "POST"),
SUBMIT_TOKEN(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken", "POST"),
// FilterApi
UPLOAD_FILTER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter", "POST"),
GET_FILTER_BY_ID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}", "GET"),
// IndentityAuthApi
IDENTITY_REGISTER(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register", "POST"),
// MediaApi
GET_MEDIA_CONFIG(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config", "GET"),
GET_PREVIEW_URL_DATA(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url", "GET"),
// OpenIdApi
OPEN_ID_TOKEN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token", "POST"),
// ProfileApi
GET_PROFILE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}", "GET"),
GET_THREE_PIDS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid", "GET"),
SET_DISPLAY_NAME(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname", "PUT"),
SET_AVATAR_URL(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url", "PUT"),
BIND_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind", "POST"),
UNBIND_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind", "POST"),
ADD_EMAIL(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken", "POST"),
ADD_MSISDN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken", "POST"),
FINALIZE_ADD_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add", "POST"),
DELETE_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete", "POST"),
// PusherRulesApi
GET_ALL_PUSHER_RULES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/", "GET"),
UPDATE_ENABLE_PUSH_RULE_STATUS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled", "PUT"),
UPDATE_PUSH_RULE_ACTIONS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions", "PUT"),
DELETE_PUSH_RULE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}", "DELETE"),
ADD_PUSH_RULE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}", "PUT"),
// PusherApi
GET_PUSHERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers", "GET"),
SET_PUSHER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set", "POST"),
// SignOutApi
LOGIN_AGAIN(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login", "POST"),
SIGN_OUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout", "POST"),
// RoomApi
GET_PUBLIC_ROOMS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms", "POST"),
CREATE_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom", "POST"),
GET_ROOM_MESSAGES_FROM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages", "GET"),
GET_MEMBERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members", "GET"),
SEND_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}", "PUT"),
GET_CONTEXT_OF_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}", "GET"),
GET_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}", "GET"),
SEND_READ_MARKER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers", "POST"),
INVITE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite", "POST"),
INVITE_USING_THREE_PID(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite", "POST"),
SEND_STATE_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}", "PUT"),
SEND_STATE_EVENT_WITH_STATE_KEY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}", "PUT"),
GET_ROOM_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state", "GET"),
SEND_RELATION(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}", "POST"),
GET_RELATIONS(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}", "GET"),
JOIN_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}", "POST"),
LEAVE_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave", "POST"),
BAN_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban", "POST"),
UNBAN_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban", "POST"),
KICK_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick", "POST"),
REDACT_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}", "PUT"),
REPORT_CONTENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}", "POST"),
GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases", "GET"),
SEND_TYPING_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}", "PUT"),
PUT_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "PUT"),
DELETE_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "DELETE"),
// SyncApi
SYNC(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync", "GET"),
// ThirdPartyApi
THIRD_PARTY_PROTOCOLS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols", "GET"),
THIRD_PARTY_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}", "GET"),
// SearchUserApi
SEARCH_USERS(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search", "POST"),
// AccountDataApi
SET_ACCOUNT_DATA(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}", "PUT")
}

View file

@ -38,7 +38,7 @@ internal class CryptoSessionInfoProvider @Inject constructor(
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
.isEmpty(EventEntityFields.STATE_KEY)
.findFirst()
}
return encryptionEvent != null

View file

@ -856,15 +856,8 @@ internal class DefaultCryptoService @Inject constructor(
return
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId)
try {
loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
}
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
}
}

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
@ -28,7 +29,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.util.logLimit
import timber.log.Timber
import javax.inject.Inject
@ -39,8 +40,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) {
private val taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(userIds: List<String>)
@ -75,8 +77,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// HS not ready for retry
private val notReadyToRetryHS = mutableSetOf<String>()
private val cryptoCoroutineContext = coroutineDispatchers.crypto
init {
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
@ -104,7 +108,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
if (':' in userId) {
try {
synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':'))
res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
}
} catch (e: Exception) {
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
@ -123,28 +127,37 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
}
fun onRoomMembersLoadedFor(roomId: String) {
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
// It's OK to track also device for invited users
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
startTrackingDeviceList(userIds)
refreshOutdatedDeviceLists()
}
}
}
/**
* Mark the cached device list for the given user outdated
* flag the given user for device-list tracking, if they are not already.
*
* @param userIds the user ids list
*/
fun startTrackingDeviceList(userIds: List<String>?) {
if (null != userIds) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
fun startTrackingDeviceList(userIds: List<String>) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
@ -155,13 +168,17 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param left the user ids list which left a room
*/
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left")
Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (changed.isNotEmpty() || left.isNotEmpty()) {
clearUnavailableServersList()
}
for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId")
Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
@ -169,7 +186,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId")
Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true
}
@ -307,7 +324,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param downloadUsers the user ids list
*/
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
// get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) {

View file

@ -39,7 +39,7 @@ internal class GossipingWorkManager @Inject constructor(
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
}

View file

@ -312,7 +312,7 @@ internal class MXOlmDevice @Inject constructor(
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return a list of known session ids for the device.
*/
fun getSessionIds(theirDeviceIdentityKey: String): Set<String>? {
fun getSessionIds(theirDeviceIdentityKey: String): List<String>? {
return store.getDeviceSessionIds(theirDeviceIdentityKey)
}

View file

@ -67,8 +67,9 @@ internal class MXMegolmEncryption(
init {
// restore existing outbound session if any
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
}
// Default rotation periods
// TODO: Make it configurable via parameters
// Session rotation periods
@ -125,6 +126,7 @@ internal class MXMegolmEncryption(
Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis")
}
/**
* Prepare a new session.
*
@ -240,6 +242,7 @@ internal class MXMegolmEncryption(
val contentMap = MXUsersDevicesMap<Any>()
var haveTargets = false
val userIds = results.userIds
val noOlmToNotify = mutableListOf<UserDevice>()
for (userId in userIds) {
val devicesToShareWith = devicesByUser[userId]
for ((deviceID) in devicesToShareWith!!) {
@ -251,13 +254,7 @@ internal class MXMegolmEncryption(
// MSC 2399
// send withheld m.no_olm: an olm session could not be established.
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
notifyKeyWithHeld(
listOf(UserDevice(userId, deviceID)),
session.sessionId,
olmDevice.deviceCurve25519Key,
WithHeldCode.NO_OLM
)
noOlmToNotify.add(UserDevice(userId, deviceID))
continue
}
Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
@ -277,14 +274,14 @@ internal class MXMegolmEncryption(
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
gossipingEventBuffer.add(
Event(
type = EventType.ROOM_KEY,
senderId = this.userId,
content = submap.apply {
this["session_key"] = ""
// we add a fake key for trail
this["_dest"] = "$userId|$deviceId"
}
))
type = EventType.ROOM_KEY,
senderId = this.userId,
content = submap.apply {
this["session_key"] = ""
// we add a fake key for trail
this["_dest"] = "$userId|$deviceId"
}
))
}
}
@ -304,6 +301,16 @@ internal class MXMegolmEncryption(
} else {
Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
}
if (noOlmToNotify.isNotEmpty()) {
// XXX offload?, as they won't read the message anyhow?
notifyKeyWithHeld(
noOlmToNotify,
session.sessionId,
olmDevice.deviceCurve25519Key,
WithHeldCode.NO_OLM
)
}
}
private suspend fun notifyKeyWithHeld(targets: List<UserDevice>,

View file

@ -154,7 +154,7 @@ internal class MXOlmDecryption(
* @return payload, if decrypted successfully.
*/
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet()
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
val messageBody = message["body"] as? String ?: return null
val messageType = when (val typeAsVoid = message["type"]) {

View file

@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
@ -750,7 +751,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users: $userIds")
Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(userIds)
@ -759,7 +760,7 @@ internal class DefaultCrossSigningService @Inject constructor(
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
.setInputData(workerData)
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager

View file

@ -33,15 +33,18 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
@ -65,11 +68,16 @@ internal class UpdateTrustWorker(context: Context,
@Inject lateinit var crossSigningService: DefaultCrossSigningService
// It breaks the crypto store contract, but we need to batch things :/
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@UserId @Inject lateinit var myUserId: String
@CryptoDatabase
@Inject lateinit var cryptoRealmConfiguration: RealmConfiguration
@SessionDatabase
@Inject lateinit var sessionRealmConfiguration: RealmConfiguration
@UserId
@Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@Inject lateinit var cryptoStore: IMXCryptoStore
@ -79,118 +87,109 @@ internal class UpdateTrustWorker(context: Context,
}
override suspend fun doSafeWork(params: Params): Result {
var userList = params.filename
val userList = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) }
?.userIds
?: params.updatedUserIds.orEmpty()
if (userList.isEmpty()) {
// This should not happen, but let's avoid go further in case of empty list
cleanup(params)
return Result.success()
// List should not be empty, but let's avoid go further in case of empty list
if (userList.isNotEmpty()) {
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/
Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
updateTrust(userList)
}
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/
cleanup(params)
return Result.success()
}
Timber.d("## CrossSigning - Updating trust for $userList")
private suspend fun updateTrust(userListParam: List<String>) {
var userList = userListParam
var myCrossSigningInfo: MXCrossSigningInfo? = null
// First we check that the users MSK are trusted by mine
// After that we check the trust chain for each devices of each users
Realm.getInstance(realmConfiguration).use { realm ->
realm.executeTransaction {
// By mapping here to model, this object is not live
// I should update it if needed
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
awaitTransaction(cryptoRealmConfiguration) { cryptoRealm ->
// By mapping here to model, this object is not live
// I should update it if needed
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
var myTrustResult: UserTrustResult? = null
var myTrustResult: UserTrustResult? = null
if (userList.contains(myUserId)) {
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust
// do it anyway ?
userList = realm.where(CrossSigningInfoEntity::class.java)
.findAll().mapNotNull { it.userId }
Timber.d("## CrossSigning - Updating trust for all $userList")
if (userList.contains(myUserId)) {
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust
// do it anyway ?
userList = cryptoRealm.where(CrossSigningInfoEntity::class.java)
.findAll()
.mapNotNull { it.userId }
// check right now my keys and mark it as trusted as other trust depends on it
val myDevices = realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, myUserId)
.findFirst()
?.devices
?.map { deviceInfo ->
CryptoMapper.mapToModel(deviceInfo)
}
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
// update model reference
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
}
// check right now my keys and mark it as trusted as other trust depends on it
val myDevices = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, myUserId)
.findFirst()
?.devices
?.map { CryptoMapper.mapToModel(it) }
val otherInfos = userList.map {
it to realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
.toMap()
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices)
updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
// update model reference
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
}
val trusts = otherInfos.map { infoEntry ->
infoEntry.key to when (infoEntry.key) {
myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
}
val otherInfos = userList.associateWith { userId ->
getCrossSigningInfo(cryptoRealm, userId)
}
val trusts = otherInfos.mapValues { entry ->
when (entry.key) {
myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
Timber.d("## CrossSigning - user:${entry.key} result:$it")
}
}
}.toMap()
}
}
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
// i have all the new trusts, update DB
trusts.forEach {
val verified = it.value?.isVerified() == true
updateCrossSigningKeysTrust(realm, it.key, verified)
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
// i have all the new trusts, update DB
trusts.forEach {
val verified = it.value?.isVerified() == true
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
}
// Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}")
trusts.keys.forEach { userId ->
val devicesEntities = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?.devices
val trustMap = devicesEntities?.associateWith { device ->
// get up to date from DB has could have been updated
val otherInfo = getCrossSigningInfo(cryptoRealm, userId)
crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
}
// Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
trusts.keys.forEach {
val devicesEntities = realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, it)
.findFirst()
?.devices
val trustMap = devicesEntities?.map { device ->
// get up to date from DB has could have been updated
val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
}?.toMap()
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
val trustEntity = device.trustLevelEntity
if (trustEntity == null) {
realm.createObject(TrustLevelEntity::class.java).let {
it.locallyVerified = false
it.crossSignedVerified = crossSignedVerified
device.trustLevelEntity = it
}
} else {
trustEntity.crossSignedVerified = crossSignedVerified
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
val trustEntity = device.trustLevelEntity
if (trustEntity == null) {
device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
it.locallyVerified = false
it.crossSignedVerified = crossSignedVerified
}
} else {
trustEntity.crossSignedVerified = crossSignedVerified
}
}
}
@ -199,37 +198,51 @@ internal class UpdateTrustWorker(context: Context,
// So Cross Signing keys trust is updated, device trust is updated
// We can now update room shields? in the session DB?
updateTrustStep2(userList, myCrossSigningInfo)
}
private suspend fun updateTrustStep2(userList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
Timber.d("## CrossSigning - Updating shields for impacted rooms...")
Realm.getInstance(sessionRealmConfiguration).use { it ->
it.executeTransaction { realm ->
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
sessionRealm.where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
.findAll()
.map { it.roomId }
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
distinctRoomIds.forEach { roomId ->
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummary?.isEncrypted == true) {
Timber.d("## CrossSigning - Check shield state for room $roomId")
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
}
} catch (failure: Throwable) {
Timber.e(failure)
.also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") }
.forEach { roomId ->
RoomSummaryEntity.where(sessionRealm, roomId)
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
.findFirst()
?.let { roomSummary ->
Timber.d("## CrossSigning - Check shield state for room $roomId")
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeRoomShield(
myCrossSigningInfo,
cryptoRealm,
allActiveRoomMembers,
roomSummary
)
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
}
} catch (failure: Throwable) {
Timber.e(failure)
}
}
}
}
}
}
}
}
cleanup(params)
return Result.success()
private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
return cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
?.let { mapCrossSigningInfoEntity(it) }
}
private fun cleanup(params: Params) {
@ -237,30 +250,34 @@ internal class UpdateTrustWorker(context: Context,
?.let { updateTrustWorkerDataRepository.delete(it) }
}
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) {
cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
xInfoEntity?.crossSigningKeys?.forEach { info ->
// optimization to avoid trigger updates when there is no change..
if (info.trustLevelEntity?.isVerified() != verified) {
Timber.d("## CrossSigning - Trust change for $userId : $verified")
val level = info.trustLevelEntity
if (level == null) {
val newLevel = realm.createObject(TrustLevelEntity::class.java)
newLevel.locallyVerified = verified
newLevel.crossSignedVerified = verified
info.trustLevelEntity = newLevel
} else {
level.locallyVerified = verified
level.crossSignedVerified = verified
?.crossSigningKeys
?.forEach { info ->
// optimization to avoid trigger updates when there is no change..
if (info.trustLevelEntity?.isVerified() != verified) {
Timber.d("## CrossSigning - Trust change for $userId : $verified")
val level = info.trustLevelEntity
if (level == null) {
info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
it.locallyVerified = verified
it.crossSignedVerified = verified
}
} else {
level.locallyVerified = verified
level.crossSignedVerified = verified
}
}
}
}
}
}
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
private fun computeRoomShield(myCrossSigningInfo: MXCrossSigningInfo?,
cryptoRealm: Realm,
activeMemberUserIds: List<String>,
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
// The set of “all users” depends on the type of room:
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
@ -272,17 +289,8 @@ internal class UpdateTrustWorker(context: Context,
val allTrustedUserIds = listToCheck
.filter { userId ->
Realm.getInstance(realmConfiguration).use {
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
}
getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
}
val myCrossKeys = Realm.getInstance(realmConfiguration).use {
it.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
}
return if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default
@ -291,21 +299,17 @@ internal class UpdateTrustWorker(context: Context,
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { uid ->
Realm.getInstance(realmConfiguration).use {
it.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, uid)
.findFirst()
?.devices
?.map {
CryptoMapper.mapToModel(it)
}
}
.mapNotNull { userId ->
cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?.devices
?.map { CryptoMapper.mapToModel(it) }
}
.flatten()
.let { allDevices ->
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
if (myCrossKeys != null) {
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}")
if (myCrossSigningInfo != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method

View file

@ -259,7 +259,7 @@ internal interface IMXCryptoStore {
* @param deviceKey the public key of the other device.
* @return A set of sessionId, or null if device is not known
*/
fun getDeviceSessionIds(deviceKey: String): Set<String>?
fun getDeviceSessionIds(deviceKey: String): List<String>?
/**
* Retrieve an end-to-end session between the logged-in user and another

View file

@ -692,7 +692,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getDeviceSessionIds(deviceKey: String): MutableSet<String> {
override fun getDeviceSessionIds(deviceKey: String): List<String> {
return doWithRealm(realmConfiguration) {
it.where<OlmSessionEntity>()
.equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey)
@ -701,7 +701,6 @@ internal class RealmCryptoStore @Inject constructor(
sessionEntity.sessionId
}
}
.toMutableSet()
}
override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) {
@ -801,7 +800,7 @@ internal class RealmCryptoStore @Inject constructor(
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
*/
override fun getInboundGroupSessions(): MutableList<OlmInboundGroupSessionWrapper2> {
override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
.findAll()
@ -809,7 +808,6 @@ internal class RealmCryptoStore @Inject constructor(
inboundGroupSessionEntity.getInboundGroupSession()
}
}
.toMutableList()
}
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
@ -964,7 +962,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getRoomsListBlacklistUnverifiedDevices(): MutableList<String> {
override fun getRoomsListBlacklistUnverifiedDevices(): List<String> {
return doWithRealm(realmConfiguration) {
it.where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true)
@ -973,10 +971,9 @@ internal class RealmCryptoStore @Inject constructor(
cryptoRoom.roomId
}
}
.toMutableList()
}
override fun getDeviceTrackingStatuses(): MutableMap<String, Int> {
override fun getDeviceTrackingStatuses(): Map<String, Int> {
return doWithRealm(realmConfiguration) {
it.where<UserEntity>()
.findAll()
@ -987,7 +984,6 @@ internal class RealmCryptoStore @Inject constructor(
entry.value.deviceTrackingStatus
}
}
.toMutableMap()
}
override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) {

View file

@ -45,7 +45,7 @@ internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) {
return CryptoCrossSigningKey(
userId = userId ?: "",
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = keyInfo.usages.map { it },
usages = keyInfo.usages.toList(),
signatures = deserializeSignatures(keyInfo.signatures),
trustLevel = keyInfo.trustLevelEntity?.let {
DeviceTrustLevel(

View file

@ -16,17 +16,25 @@
package org.matrix.android.sdk.internal.crypto.tasks
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.computeBestChunkSize
import javax.inject.Inject
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
data class Params(
// the list of users to get keys for.
// the list of users to get keys for. The list MUST NOT be empty
val userIds: List<String>,
// the up-to token
val token: String?
@ -39,15 +47,68 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(
) : DownloadKeysForUsersTask {
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
val downloadQuery = params.userIds.associateWith { emptyList<String>() }
val bestChunkSize = computeBestChunkSize(params.userIds.size, LIMIT)
val token = params.token?.takeIf { token -> token.isNotEmpty() }
val body = KeysQueryBody(
deviceKeys = downloadQuery,
token = params.token?.takeIf { it.isNotEmpty() }
)
return if (bestChunkSize.shouldChunk()) {
// Store server results in these mutable maps
val deviceKeys = mutableMapOf<String, Map<String, DeviceKeysWithUnsigned>>()
val failures = mutableMapOf<String, Map<String, Any>>()
val masterKeys = mutableMapOf<String, RestKeyInfo?>()
val selfSigningKeys = mutableMapOf<String, RestKeyInfo?>()
val userSigningKeys = mutableMapOf<String, RestKeyInfo?>()
return executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.downloadKeysForUsers(body)
val mutex = Mutex()
// Split network request into smaller request (#2925)
coroutineScope {
params.userIds
.chunked(bestChunkSize.chunkSize)
.map {
KeysQueryBody(
deviceKeys = it.associateWith { emptyList() },
token = token
)
}
.map { body ->
async {
val result = executeRequest<KeysQueryResponse>(globalErrorReceiver) {
apiCall = cryptoApi.downloadKeysForUsers(body)
}
mutex.withLock {
deviceKeys.putAll(result.deviceKeys.orEmpty())
failures.putAll(result.failures.orEmpty())
masterKeys.putAll(result.masterKeys.orEmpty())
selfSigningKeys.putAll(result.selfSigningKeys.orEmpty())
userSigningKeys.putAll(result.userSigningKeys.orEmpty())
}
}
}
.joinAll()
}
KeysQueryResponse(
deviceKeys = deviceKeys,
failures = failures,
masterKeys = masterKeys,
selfSigningKeys = selfSigningKeys,
userSigningKeys = userSigningKeys
)
} else {
// No need to chunk, direct request
executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.downloadKeysForUsers(
KeysQueryBody(
deviceKeys = params.userIds.associateWith { emptyList() },
token = token
)
)
}
}
}
companion object {
const val LIMIT = 250
}
}

View file

@ -183,7 +183,7 @@ internal class VerificationTransportRoomMessage(
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(workerParams)
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager
@ -280,7 +280,7 @@ internal class VerificationTransportRoomMessage(
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(workerParams)
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
return workManagerProvider.workManager
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)

View file

@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingIntercept
import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.matrix.android.sdk.internal.network.ApiInterceptor
import java.util.concurrent.TimeUnit
@Module
@ -63,7 +64,8 @@ internal object NetworkModule {
timeoutInterceptor: TimeOutInterceptor,
userAgentInterceptor: UserAgentInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
curlLoggingInterceptor: CurlLoggingInterceptor): OkHttpClient {
curlLoggingInterceptor: CurlLoggingInterceptor,
apiInterceptor: ApiInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
@ -76,6 +78,7 @@ internal object NetworkModule {
.addInterceptor(timeoutInterceptor)
.addInterceptor(userAgentInterceptor)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(apiInterceptor)
.apply {
if (BuildConfig.LOG_PRIVATE_DATA) {
addInterceptor(curlLoggingInterceptor)

View file

@ -23,6 +23,7 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkRequest
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -69,6 +70,7 @@ internal class WorkManagerProvider @Inject constructor(
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
const val BACKOFF_DELAY = 10_000L
// Use min value, smaller value will be ignored
const val BACKOFF_DELAY_MILLIS = WorkRequest.MIN_BACKOFF_MILLIS
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.network
import okhttp3.Interceptor
import okhttp3.Response
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.network.ApiInterceptorListener
import org.matrix.android.sdk.api.network.ApiPath
import org.matrix.android.sdk.internal.di.MatrixScope
import timber.log.Timber
import javax.inject.Inject
/**
* Interceptor class for provided api paths.
*/
@MatrixScope
internal class ApiInterceptor @Inject constructor() : Interceptor {
init {
Timber.d("ApiInterceptor.init")
}
private val apiResponseListenersMap = mutableMapOf<ApiPath, MutableList<ApiInterceptorListener>>()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val path = request.url.encodedPath.replaceFirst("/", "")
val method = request.method
val response = chain.proceed(request)
synchronized(apiResponseListenersMap) {
findApiPath(path, method)?.let { apiPath ->
response.peekBody(Long.MAX_VALUE).string().let { networkResponse ->
apiResponseListenersMap[apiPath]?.forEach { listener ->
tryOrNull("Error in the implementation") {
listener.onApiResponse(apiPath, networkResponse)
}
}
}
}
}
return response
}
private fun findApiPath(path: String, method: String): ApiPath? {
return apiResponseListenersMap
.keys
.find { apiPath ->
apiPath.method === method && isTheSamePath(apiPath.path, path)
}
}
private fun isTheSamePath(pattern: String, path: String): Boolean {
val patternSegments = pattern.split("/")
val pathSegments = path.split("/")
if (patternSegments.size != pathSegments.size) return false
return patternSegments.indices.all { i ->
patternSegments[i] == pathSegments[i] || patternSegments[i].startsWith("{")
}
}
/**
* Adds listener to send intercepted api responses through.
*/
fun addListener(path: ApiPath, listener: ApiInterceptorListener) {
synchronized(apiResponseListenersMap) {
apiResponseListenersMap.getOrPut(path) { mutableListOf() }
.add(listener)
}
}
/**
* Remove listener to send intercepted api responses through.
*/
fun removeListener(path: ApiPath, listener: ApiInterceptorListener) {
synchronized(apiResponseListenersMap) {
apiResponseListenersMap[path]?.remove(listener)
if (apiResponseListenersMap[path]?.isEmpty() == true) {
apiResponseListenersMap.remove(path)
}
}
}
}

View file

@ -96,7 +96,7 @@ internal class DefaultPushersService @Inject constructor(
val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddHttpPusherWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(WorkerParamsFactory.toData(params))
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager.enqueue(request)
return request.id

View file

@ -53,8 +53,9 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import timber.log.Timber
import javax.inject.Inject
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
: EventInsertLiveProcessor {
internal class EventRelationsAggregationProcessor @Inject constructor(
@UserId private val userId: String
) : EventInsertLiveProcessor {
private val allowedTypes = listOf(
EventType.MESSAGE,
@ -87,12 +88,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
EventType.REACTION -> {
// we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
handleReaction(event, roomId, realm, userId, isLocalEcho)
handleReaction(realm, event, roomId, isLocalEcho)
}
EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
?.let {
@ -108,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
handleReplace(realm, event, content, roomId, isLocalEcho)
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
handleResponse(realm, event, content, roomId, isLocalEcho)
}
}
@ -122,7 +123,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
if (it.type == RelationType.REFERENCE && it.eventId != null) {
handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId)
handleVerification(realm, event, roomId, isLocalEcho, it.eventId)
}
}
}
@ -140,7 +141,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
@ -154,10 +155,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
EventType.KEY_VERIFICATION_KEY -> {
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
encryptedEventContent.relatesTo.eventId?.let {
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
handleVerification(realm, event, roomId, isLocalEcho, it)
}
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
// Reaction
if (event.getClearType() == EventType.REACTION) {
// we got a reaction!!
Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho)
}
}
}
EventType.REDACTION -> {
@ -172,11 +180,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
// was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
handleRedactionOfReplace(realm, eventToPrune, contentModel.relatesTo!!.eventId!!)
}
}
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId)
handleReactionRedact(realm, eventToPrune)
}
}
}
@ -267,7 +275,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
}
private fun handleResponse(realm: Realm,
userId: String,
event: Event,
content: MessageContent,
roomId: String,
@ -354,7 +361,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
}
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
private fun handleInitialAggregatedRelations(realm: Realm,
event: Event,
roomId: String,
aggregation: AggregatedAnnotation) {
if (SHOULD_HANDLE_SERVER_AGREGGATION) {
aggregation.chunk?.forEach {
if (it.type == EventType.REACTION) {
@ -376,7 +386,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
}
}
private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
private fun handleReaction(realm: Realm,
event: Event,
roomId: String,
isLocalEcho: Boolean) {
val content = event.content.toModel<ReactionContent>()
if (content == null) {
Timber.e("Malformed reaction content ${event.content}")
@ -441,7 +454,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
/**
* Called when an event is deleted
*/
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
private fun handleRedactionOfReplace(realm: Realm,
redacted: EventEntity,
relatedEventId: String) {
Timber.d("Handle redaction of m.replace")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, redacted.roomId, relatedEventId).findFirst()
if (eventSummary == null) {
@ -457,7 +472,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
sourceToDiscard.deleteFromRealm()
}
private fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
private fun handleReactionRedact(realm: Realm,
eventToPrune: EventEntity) {
Timber.v("REDACTION of reaction ${eventToPrune.eventId}")
// delete a reaction, need to update the annotation summary if any
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() ?: return
@ -494,7 +510,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
}
}
private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String) {
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
val verifSummary = eventSummary.referencesSummaryEntity

View file

@ -22,6 +22,8 @@ import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
@ -57,6 +59,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val deviceListManager: DeviceListManager,
private val globalErrorReceiver: GlobalErrorReceiver
) : LoadRoomMembersTask {
@ -124,6 +128,10 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
roomSummaryUpdater.update(realm, roomId, updateMembers = true)
}
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
deviceListManager.onRoomMembersLoadedFor(roomId)
}
}
private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {

View file

@ -117,7 +117,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
}
if (readReceiptId != null) {
val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId)
readReceiptHandler.handle(realm, roomId, readReceiptContent, false)
readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
}
if (shouldUpdateRoomSummary) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()

View file

@ -318,7 +318,7 @@ internal class DefaultSendService @AssistedInject constructor(
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(true)
.setInputData(uploadWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
}
@ -332,7 +332,7 @@ internal class DefaultSendService @AssistedInject constructor(
// .setConstraints(WorkManagerProvider.workConstraints)
.startChain(false)
.setInputData(workData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
}
}

View file

@ -131,8 +131,8 @@ internal class RoomSummaryUpdater @Inject constructor(
// mmm i want to decrypt now or is it ok to do it async?
tryOrNull {
eventDecryptor.decryptEvent(root.asDomain(), "")
// eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
}
?.let { root.setDecryptionResult(it) }
}
if (updateMembers) {
@ -144,7 +144,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) {
if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) {
// mmm maybe we could only refresh shield instead of checking trust also?
crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
}

View file

@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer
@ -73,7 +74,8 @@ internal class DefaultTimeline(
private val timelineInput: TimelineInput,
private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : Timeline,
TimelineHiddenReadReceipts.Delegate,
TimelineInput.Listener,
@ -182,11 +184,27 @@ internal class DefaultTimeline(
}
.executeBy(taskExecutor)
// Ensure ReadReceipt from init sync are loaded
ensureReadReceiptAreLoaded(realm)
isReady.set(true)
}
}
}
private fun ensureReadReceiptAreLoaded(realm: Realm) {
readReceiptHandler.getContentFromInitSync(roomId)
?.also {
Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
}
?.let { readReceiptContent ->
realm.executeTransactionAsync {
readReceiptHandler.handle(it, roomId, readReceiptContent, false, null)
readReceiptHandler.onContentFromInitSyncHandled(roomId)
}
}
}
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
}

View file

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.session.room.timeline
import androidx.lifecycle.LiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.Sort
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.isImageMessage
@ -38,20 +38,23 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val timelineInput: TimelineInput,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask
internal class DefaultTimelineService @AssistedInject constructor(
@Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val timelineInput: TimelineInput,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : TimelineService {
@AssistedFactory
@ -74,7 +77,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask
loadRoomMembersTask = loadRoomMembersTask,
readReceiptHandler = readReceiptHandler
)
}

View file

@ -50,7 +50,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
}
@ -60,6 +60,5 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
companion object {
private const val SEND_WORK = "SEND_WORK"
private const val BACKOFF_DELAY = 10_000L
}
}

View file

@ -65,7 +65,7 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta
val state = cache?.step ?: InitialSyncStatus.STEP_INIT
return if (state >= InitialSyncStatus.STEP_DOWNLOADED
&& System.currentTimeMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) {
Timber.v("INIT_SYNC downloaded file is outdated, download it again")
Timber.d("INIT_SYNC downloaded file is outdated, download it again")
// The downloaded file is outdated
setStep(InitialSyncStatus.STEP_INIT)
InitialSyncStatus.STEP_INIT

View file

@ -42,9 +42,9 @@ sealed class InitialSyncStrategy {
val minSizeToSplit: Long = 1024 * 1024,
/**
* Limit per room to reach to decide to store a join room ephemeral Events into a file
* Empiric value: 6 kilobytes
* Empiric value: 1 kilobytes
*/
val minSizeToStoreInFile: Long = 6 * 1024,
val minSizeToStoreInFile: Long = 1024,
/**
* Max number of rooms to insert at a time in database (to avoid too much RAM usage)
*/

View file

@ -16,12 +16,13 @@
package org.matrix.android.sdk.internal.session.sync
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.query.createUnmanaged
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@ -35,7 +36,9 @@ typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, D
private const val READ_KEY = "m.read"
private const val TIMESTAMP_KEY = "ts"
internal class ReadReceiptHandler @Inject constructor() {
internal class ReadReceiptHandler @Inject constructor(
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
companion object {
@ -52,22 +55,29 @@ internal class ReadReceiptHandler @Inject constructor() {
}
}
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) {
if (content == null) {
return
}
fun handle(realm: Realm,
roomId: String,
content: ReadReceiptContent?,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator?) {
content ?: return
try {
handleReadReceiptContent(realm, roomId, content, isInitialSync)
handleReadReceiptContent(realm, roomId, content, isInitialSync, aggregator)
} catch (exception: Exception) {
Timber.e("Fail to handle read receipt for room $roomId")
}
}
private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) {
private fun handleReadReceiptContent(realm: Realm,
roomId: String,
content: ReadReceiptContent,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator?) {
if (isInitialSync) {
initialSyncStrategy(realm, roomId, content)
} else {
incrementalSyncStrategy(realm, roomId, content)
incrementalSyncStrategy(realm, roomId, content, aggregator)
}
}
@ -87,7 +97,21 @@ internal class ReadReceiptHandler @Inject constructor() {
realm.insertOrUpdate(readReceiptSummaries)
}
private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
private fun incrementalSyncStrategy(realm: Realm,
roomId: String,
content: ReadReceiptContent,
aggregator: SyncResponsePostTreatmentAggregator?) {
// First check if we have data from init sync to handle
getContentFromInitSync(roomId)?.let {
Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId")
doIncrementalSyncStrategy(realm, roomId, it)
aggregator?.ephemeralFilesToDelete?.add(roomId)
}
doIncrementalSyncStrategy(realm, roomId, content)
}
private fun doIncrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
@ -110,4 +134,27 @@ internal class ReadReceiptHandler @Inject constructor() {
}
}
}
fun getContentFromInitSync(roomId: String): ReadReceiptContent? {
val dataFromFile = roomSyncEphemeralTemporaryStore.read(roomId)
dataFromFile ?: return null
@Suppress("UNCHECKED_CAST")
val content = dataFromFile
.events
.firstOrNull { it.type == EventType.RECEIPT }
?.content as? ReadReceiptContent
if (content == null) {
// We can delete the file now
roomSyncEphemeralTemporaryStore.delete(roomId)
}
return content
}
fun onContentFromInitSyncHandled(roomId: String) {
roomSyncEphemeralTemporaryStore.delete(roomId)
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import okio.buffer
import okio.source
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
import org.matrix.android.sdk.internal.util.md5
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal interface RoomSyncEphemeralTemporaryStore {
fun write(roomId: String, roomSyncEphemeralJson: String)
fun read(roomId: String): RoomSyncEphemeral?
fun reset()
fun delete(roomId: String)
}
internal class RoomSyncEphemeralTemporaryStoreFile @Inject constructor(
@SessionFilesDirectory fileDirectory: File,
moshi: Moshi
) : RoomSyncEphemeralTemporaryStore {
private val workingDir = File(fileDirectory, "rr")
.also { it.mkdirs() }
private val roomSyncEphemeralAdapter = moshi.adapter(RoomSyncEphemeral::class.java)
/**
* Write RoomSyncEphemeral to a file
*/
override fun write(roomId: String, roomSyncEphemeralJson: String) {
Timber.w("INIT_SYNC Store ephemeral events for room $roomId")
getFile(roomId).writeText(roomSyncEphemeralJson)
}
/**
* Read RoomSyncEphemeral from a file, or null if there is no file to read
*/
override fun read(roomId: String): RoomSyncEphemeral? {
return getFile(roomId)
.takeIf { it.exists() }
?.inputStream()
?.use { pos ->
roomSyncEphemeralAdapter.fromJson(JsonReader.of(pos.source().buffer()))
}
}
override fun delete(roomId: String) {
getFile(roomId).delete()
}
override fun reset() {
workingDir.deleteRecursively()
workingDir.mkdirs()
}
private fun getFile(roomId: String): File {
return File(workingDir, "${roomId.md5()}.json")
}
}

View file

@ -60,12 +60,13 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.internal.util.computeBestChunkSize
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.ceil
internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
@ -87,29 +88,21 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
fun handle(realm: Realm,
roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter? = null) {
Timber.v("Execute transaction from $this")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
}
fun handleInitSyncEphemeral(realm: Realm,
roomsSyncResponse: RoomsSyncResponse) {
roomsSyncResponse.join.forEach { roomSync ->
val ephemeralResult = roomSync.value.ephemeral
?.roomSyncEphemeral
?.events
?.takeIf { it.isNotEmpty() }
?.let { events -> handleEphemeral(realm, roomSync.key, events, true) }
roomTypingUsersHandler.handle(realm, roomSync.key, ephemeralResult)
}
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
}
// PRIVATE METHODS *****************************************************************************
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: ProgressReporter?) {
private fun handleRoomSync(realm: Realm,
handlingStrategy: HandlingStrategy,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
val insertType = if (isInitialSync) {
EventInsertType.INITIAL_SYNC
} else {
@ -119,12 +112,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED -> {
if (isInitialSync && initialSyncStrategy is InitialSyncStrategy.Optimized) {
insertJoinRoomsFromInitSync(realm, handlingStrategy, syncLocalTimeStampMillis, reporter)
insertJoinRoomsFromInitSync(realm, handlingStrategy, syncLocalTimeStampMillis, aggregator, reporter)
// Rooms are already inserted, return an empty list
emptyList()
} else {
handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) {
handleJoinedRoom(realm, it.key, it.value, true, insertType, syncLocalTimeStampMillis)
handleJoinedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis, aggregator)
}
}
}
@ -145,29 +138,30 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun insertJoinRoomsFromInitSync(realm: Realm,
handlingStrategy: HandlingStrategy.JOINED,
syncLocalTimeStampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
val maxSize = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
val listSize = handlingStrategy.data.keys.size
val numberOfChunks = ceil(listSize / maxSize.toDouble()).toInt()
val bestChunkSize = computeBestChunkSize(
listSize = handlingStrategy.data.keys.size,
limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
)
if (numberOfChunks > 1) {
reportSubtask(reporter, InitSyncStep.ImportingAccountJoinedRooms, numberOfChunks, 0.6f) {
val chunkSize = listSize / numberOfChunks
Timber.v("INIT_SYNC $listSize rooms to insert, split into $numberOfChunks sublists of $chunkSize items")
if (bestChunkSize.shouldChunk()) {
reportSubtask(reporter, InitSyncStep.ImportingAccountJoinedRooms, bestChunkSize.numberOfChunks, 0.6f) {
Timber.d("INIT_SYNC ${handlingStrategy.data.keys.size} rooms to insert, split with $bestChunkSize")
// I cannot find a better way to chunk a map, so chunk the keys and then create new maps
handlingStrategy.data.keys
.chunked(chunkSize)
.chunked(bestChunkSize.chunkSize)
.forEachIndexed { index, roomIds ->
val roomEntities = roomIds
.also { Timber.v("INIT_SYNC insert ${roomIds.size} rooms") }
.also { Timber.d("INIT_SYNC insert ${roomIds.size} rooms") }
.map {
handleJoinedRoom(
realm = realm,
roomId = it,
roomSync = handlingStrategy.data[it] ?: error("Should not happen"),
handleEphemeralEvents = false,
insertType = EventInsertType.INITIAL_SYNC,
syncLocalTimestampMillis = syncLocalTimeStampMillis
syncLocalTimestampMillis = syncLocalTimeStampMillis,
aggregator
)
}
realm.insertOrUpdate(roomEntities)
@ -177,7 +171,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} else {
// No need to split
val rooms = handlingStrategy.data.mapWithProgress(reporter, InitSyncStep.ImportingAccountJoinedRooms, 0.6f) {
handleJoinedRoom(realm, it.key, it.value, false, EventInsertType.INITIAL_SYNC, syncLocalTimeStampMillis)
handleJoinedRoom(realm, it.key, it.value, EventInsertType.INITIAL_SYNC, syncLocalTimeStampMillis, aggregator)
}
realm.insertOrUpdate(rooms)
}
@ -186,17 +180,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync,
handleEphemeralEvents: Boolean,
insertType: EventInsertType,
syncLocalTimestampMillis: Long): RoomEntity {
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
Timber.v("Handle join sync for room $roomId")
var ephemeralResult: EphemeralResult? = null
if (handleEphemeralEvents) {
ephemeralResult = roomSync.ephemeral?.roomSyncEphemeral?.events
?.takeIf { it.isNotEmpty() }
?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC) }
}
val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
?._roomSyncEphemeral
?.events
?.takeIf { it.isNotEmpty() }
?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC, aggregator) }
if (roomSync.accountData?.events?.isNotEmpty() == true) {
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
@ -436,14 +429,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private fun handleEphemeral(realm: Realm,
roomId: String,
ephemeralEvents: List<Event>,
isInitialSync: Boolean): EphemeralResult {
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator): EphemeralResult {
var result = EphemeralResult()
for (event in ephemeralEvents) {
when (event.type) {
EventType.RECEIPT -> {
@Suppress("UNCHECKED_CAST")
(event.content as? ReadReceiptContent)?.let { readReceiptContent ->
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync, aggregator)
}
}
EventType.TYPING -> {

View file

@ -26,6 +26,7 @@ import javax.inject.Inject
internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String,
private val typingUsersTracker: DefaultTypingUsersTracker) {
// TODO This could be handled outside of the Realm transaction. Use the new aggregator?
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()

View file

@ -37,4 +37,7 @@ internal abstract class SyncModule {
@Binds
abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask
@Binds
abstract fun bindRoomSyncEphemeralTemporaryStore(store: RoomSyncEphemeralTemporaryStoreFile): RoomSyncEphemeralTemporaryStore
}

View file

@ -41,17 +41,19 @@ import kotlin.system.measureTimeMillis
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
internal class SyncResponseHandler @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
@SessionId private val sessionId: String,
private val workManagerProvider: WorkManagerProvider,
private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService) {
internal class SyncResponseHandler @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
@SessionId private val sessionId: String,
private val workManagerProvider: WorkManagerProvider,
private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler,
private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService) {
suspend fun handleResponse(syncResponse: SyncResponse,
fromToken: String?,
@ -81,13 +83,14 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
}.also {
Timber.v("Finish handling toDevice in $it ms")
}
val aggregator = SyncResponsePostTreatmentAggregator()
// Start one big transaction
monarchy.awaitTransaction { realm ->
measureTimeMillis {
Timber.v("Handle rooms")
reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) {
if (syncResponse.rooms != null) {
roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter)
roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, aggregator, reporter)
}
}
}.also {
@ -115,7 +118,10 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
}
tokenStore.saveToken(realm, syncResponse.nextBatch)
}
// Everything else we need to do outside the transaction
aggregatorHandler.handle(aggregator)
syncResponse.rooms?.let {
checkPushRules(it, isInitialSync)
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
@ -128,15 +134,6 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
cryptoSyncHandler.onSyncCompleted(syncResponse)
}
suspend fun handleInitSyncSecondTransaction(syncResponse: SyncResponse) {
// Start another transaction to handle the ephemeral events
monarchy.awaitTransaction { realm ->
if (syncResponse.rooms != null) {
roomSyncHandler.handleInitSyncEphemeral(realm, syncResponse.rooms)
}
}
}
/**
* At the moment we don't get any group data through the sync, so we poll where every hour.
* You can also force to refetch group data using [Group] API.

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
internal class SyncResponsePostTreatmentAggregator {
// List of RoomId
val ephemeralFilesToDelete = mutableListOf<String>()
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync
import javax.inject.Inject
internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
fun handle(synResHaResponsePostTreatmentAggregator: SyncResponsePostTreatmentAggregator) {
cleanupEphemeralFiles(synResHaResponsePostTreatmentAggregator.ephemeralFilesToDelete)
}
private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) {
ephemeralFilesToDelete.forEach {
ephemeralTemporaryStore.delete(it)
}
}
}

View file

@ -62,7 +62,8 @@ internal class DefaultSyncTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver,
@SessionFilesDirectory
private val fileDirectory: File,
private val syncResponseParser: InitialSyncResponseParser
private val syncResponseParser: InitialSyncResponseParser,
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) : SyncTask {
private val workingDir = File(fileDirectory, "is")
@ -100,15 +101,18 @@ internal class DefaultSyncTask @Inject constructor(
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
if (isInitialSync) {
Timber.v("INIT_SYNC with filter: ${requestParams["filter"]}")
Timber.d("INIT_SYNC with filter: ${requestParams["filter"]}")
val initSyncStrategy = initialSyncStrategy
var syncResp: SyncResponse? = null
logDuration("INIT_SYNC strategy: $initSyncStrategy") {
if (initSyncStrategy is InitialSyncStrategy.Optimized) {
roomSyncEphemeralTemporaryStore.reset()
workingDir.mkdirs()
val file = downloadInitSyncResponse(requestParams)
syncResp = reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) {
reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) {
handleSyncFile(file, initSyncStrategy)
}
// Delete all files
workingDir.deleteRecursively()
} else {
val syncResponse = logDuration("INIT_SYNC Request") {
executeRequest<SyncResponse>(globalErrorReceiver) {
@ -125,15 +129,6 @@ internal class DefaultSyncTask @Inject constructor(
}
}
initialSyncProgressService.endAll()
if (initSyncStrategy is InitialSyncStrategy.Optimized) {
logDuration("INIT_SYNC Handle ephemeral") {
syncResponseHandler.handleInitSyncSecondTransaction(syncResp!!)
}
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
// Delete all files
workingDir.deleteRecursively()
}
} else {
val syncResponse = executeRequest<SyncResponse>(globalErrorReceiver) {
apiCall = syncAPI.sync(
@ -147,11 +142,10 @@ internal class DefaultSyncTask @Inject constructor(
}
private suspend fun downloadInitSyncResponse(requestParams: Map<String, String>): File {
workingDir.mkdirs()
val workingFile = File(workingDir, "initSync.json")
val status = initialSyncStatusRepository.getStep()
if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) {
Timber.v("INIT_SYNC file is already here")
Timber.d("INIT_SYNC file is already here")
reportSubtask(initialSyncProgressService, InitSyncStep.Downloading, 1, 0.3f) {
// Empty task
}
@ -201,8 +195,8 @@ internal class DefaultSyncTask @Inject constructor(
}
}
private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse {
return logDuration("INIT_SYNC handleSyncFile()") {
private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) {
logDuration("INIT_SYNC handleSyncFile()") {
val syncResponse = logDuration("INIT_SYNC Read file and parse") {
syncResponseParser.parse(initSyncStrategy, workingFile)
}
@ -210,12 +204,12 @@ internal class DefaultSyncTask @Inject constructor(
// Log some stats
val nbOfJoinedRooms = syncResponse.rooms?.join?.size ?: 0
val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it.ephemeral is LazyRoomSyncEphemeral.Stored }
Timber.v("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files")
Timber.d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files")
logDuration("INIT_SYNC Database insertion") {
syncResponseHandler.handleResponse(syncResponse, null, initialSyncProgressService)
}
syncResponse
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
}
}

View file

@ -106,7 +106,7 @@ internal class SyncWorker(context: Context,
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setInputData(data)
.build()
workManagerProvider.workManager
@ -118,7 +118,7 @@ internal class SyncWorker(context: Context,
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
.build()

View file

@ -16,28 +16,10 @@
package org.matrix.android.sdk.internal.session.sync.model
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader
import okio.buffer
import okio.source
import java.io.File
@JsonClass(generateAdapter = false)
internal sealed class LazyRoomSyncEphemeral {
data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
data class Stored(val roomSyncEphemeralAdapter: JsonAdapter<RoomSyncEphemeral>, val file: File) : LazyRoomSyncEphemeral()
val roomSyncEphemeral: RoomSyncEphemeral
get() {
return when (this) {
is Parsed -> _roomSyncEphemeral
is Stored -> {
// Parse the file now
file.inputStream().use { pos ->
roomSyncEphemeralAdapter.fromJson(JsonReader.of(pos.source().buffer()))!!
}
}
}
}
object Stored : LazyRoomSyncEphemeral()
}

View file

@ -22,11 +22,10 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
internal class DefaultLazyRoomSyncEphemeralJsonAdapter {
@ -44,32 +43,26 @@ internal class DefaultLazyRoomSyncEphemeralJsonAdapter {
}
}
internal class SplitLazyRoomSyncJsonAdapter(
private val workingDirectory: File,
internal class SplitLazyRoomSyncEphemeralJsonAdapter(
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
private val syncStrategy: InitialSyncStrategy.Optimized
) {
private val atomicInteger = AtomicInteger(0)
private fun createFile(): File {
val index = atomicInteger.getAndIncrement()
return File(workingDirectory, "room_$index.json")
}
@FromJson
fun fromJson(reader: JsonReader, delegate: JsonAdapter<RoomSyncEphemeral>): LazyRoomSyncEphemeral? {
val path = reader.path
val roomId = path.substringAfter("\$.rooms.join.").substringBeforeLast(".ephemeral")
val json = reader.nextSource().inputStream().bufferedReader().use {
it.readText()
}
val limit = syncStrategy.minSizeToStoreInFile
return if (json.length > limit) {
Timber.v("INIT_SYNC $path content length: ${json.length} copy to a file")
Timber.d("INIT_SYNC $path content length: ${json.length} copy to a file")
// Copy the source to a file
val file = createFile()
file.writeText(json)
LazyRoomSyncEphemeral.Stored(delegate, file)
roomSyncEphemeralTemporaryStore.write(roomId, json)
LazyRoomSyncEphemeral.Stored
} else {
Timber.v("INIT_SYNC $path content length: ${json.length} parse it now")
Timber.d("INIT_SYNC $path content length: ${json.length} parse it now")
val roomSync = delegate.fromJson(json) ?: return null
LazyRoomSyncEphemeral.Parsed(roomSync)
}

View file

@ -20,29 +20,33 @@ import com.squareup.moshi.Moshi
import okio.buffer
import okio.source
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal class InitialSyncResponseParser @Inject constructor(private val moshi: Moshi) {
internal class InitialSyncResponseParser @Inject constructor(
private val moshi: Moshi,
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
) {
fun parse(syncStrategy: InitialSyncStrategy.Optimized, workingFile: File): SyncResponse {
val syncResponseLength = workingFile.length().toInt()
Timber.v("INIT_SYNC Sync file size is $syncResponseLength bytes")
Timber.d("INIT_SYNC Sync file size is $syncResponseLength bytes")
val shouldSplit = syncResponseLength >= syncStrategy.minSizeToSplit
Timber.v("INIT_SYNC should split in several files: $shouldSplit")
return getMoshi(syncStrategy, workingFile.parentFile!!, shouldSplit)
Timber.d("INIT_SYNC should split in several files: $shouldSplit")
return getMoshi(syncStrategy, shouldSplit)
.adapter(SyncResponse::class.java)
.fromJson(workingFile.source().buffer())!!
}
private fun getMoshi(syncStrategy: InitialSyncStrategy.Optimized, workingDirectory: File, shouldSplit: Boolean): Moshi {
private fun getMoshi(syncStrategy: InitialSyncStrategy.Optimized, shouldSplit: Boolean): Moshi {
// If we don't have to split we'll rely on the already default moshi
if (!shouldSplit) return moshi
// Otherwise, we create a new adapter for handling Map of Lazy sync
return moshi.newBuilder()
.add(SplitLazyRoomSyncJsonAdapter(workingDirectory, syncStrategy))
.add(SplitLazyRoomSyncEphemeralJsonAdapter(roomSyncEphemeralTemporaryStore, syncStrategy))
.build()
}
}

View file

@ -19,15 +19,27 @@ package org.matrix.android.sdk.internal.util
import org.matrix.android.sdk.BuildConfig
import timber.log.Timber
internal fun <T> Collection<T>.logLimit(maxQuantity: Int = 5): String {
return buildString {
append(size)
append(" item(s)")
if (size > maxQuantity) {
append(", first $maxQuantity items")
}
append(": ")
append(this@logLimit.take(maxQuantity))
}
}
internal suspend fun <T> logDuration(message: String,
block: suspend () -> T): T {
Timber.v("$message -- BEGIN")
Timber.d("$message -- BEGIN")
val start = System.currentTimeMillis()
val result = logRamUsage(message) {
block()
}
val duration = System.currentTimeMillis() - start
Timber.v("$message -- END duration: $duration ms")
Timber.d("$message -- END duration: $duration ms")
return result
}
@ -38,12 +50,12 @@ internal suspend fun <T> logRamUsage(message: String, block: suspend () -> T): T
runtime.gc()
val freeMemoryInMb = runtime.freeMemory() / 1048576L
val usedMemInMBStart = runtime.totalMemory() / 1048576L - freeMemoryInMb
Timber.v("$message -- BEGIN (free memory: $freeMemoryInMb MB)")
Timber.d("$message -- BEGIN (free memory: $freeMemoryInMb MB)")
val result = block()
runtime.gc()
val usedMemInMBEnd = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L
val usedMemInMBDiff = usedMemInMBEnd - usedMemInMBStart
Timber.v("$message -- END RAM usage: $usedMemInMBDiff MB")
Timber.d("$message -- END RAM usage: $usedMemInMBDiff MB")
result
} else {
block()

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util
import kotlin.math.ceil
internal data class BestChunkSize(
val numberOfChunks: Int,
val chunkSize: Int
) {
fun shouldChunk() = numberOfChunks > 1
}
internal fun computeBestChunkSize(listSize: Int, limit: Int): BestChunkSize {
return if (listSize <= limit) {
BestChunkSize(
numberOfChunks = 1,
chunkSize = listSize
)
} else {
val numberOfChunks = ceil(listSize / limit.toDouble()).toInt()
// Round on next Int
val chunkSize = ceil(listSize / numberOfChunks.toDouble()).toInt()
BestChunkSize(
numberOfChunks = numberOfChunks,
chunkSize = chunkSize
)
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldHaveSize
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.MatrixTest
@FixMethodOrder(MethodSorters.JVM)
class MathUtilTest : MatrixTest {
@Test
fun testComputeBestChunkSize0() = doTest(0, 100, 1, 0)
@Test
fun testComputeBestChunkSize1to99() {
for (i in 1..99) {
doTest(i, 100, 1, i)
}
}
@Test
fun testComputeBestChunkSize100() = doTest(100, 100, 1, 100)
@Test
fun testComputeBestChunkSize101() = doTest(101, 100, 2, 51)
@Test
fun testComputeBestChunkSize199() = doTest(199, 100, 2, 100)
@Test
fun testComputeBestChunkSize200() = doTest(200, 100, 2, 100)
@Test
fun testComputeBestChunkSize201() = doTest(201, 100, 3, 67)
@Test
fun testComputeBestChunkSize240() = doTest(240, 100, 3, 80)
private fun doTest(listSize: Int, limit: Int, expectedNumberOfChunks: Int, expectedChunkSize: Int) {
val result = computeBestChunkSize(listSize, limit)
result.numberOfChunks shouldBeEqualTo expectedNumberOfChunks
result.chunkSize shouldBeEqualTo expectedChunkSize
// Test that the result make sense, when we use chunked()
if (result.chunkSize > 0) {
generateSequence { "a" }
.take(listSize)
.chunked(result.chunkSize)
.shouldHaveSize(result.numberOfChunks)
}
}
}

View file

@ -43,7 +43,7 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment-ktx:1.3.0"
implementation "androidx.fragment:fragment-ktx:1.3.2"
implementation 'androidx.exifinterface:exifinterface:1.3.2'
// Log

View file

@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===92
enum class===93
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

View file

@ -14,7 +14,7 @@ kapt {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 1
ext.versionPatch = 2
ext.versionPatch = 4
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -290,8 +290,8 @@ android {
dependencies {
def epoxy_version = '4.4.2'
def fragment_version = '1.3.0'
def epoxy_version = '4.4.3'
def fragment_version = '1.3.2'
def arrow_version = "0.8.2"
def markwon_version = '4.1.2'
def big_image_viewer_version = '1.7.1'
@ -320,7 +320,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.recyclerview:recyclerview:1.2.0-beta02"
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
@ -342,7 +342,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.5.1'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.19'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.20'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

View file

@ -19,6 +19,7 @@ package im.vector.app.core.epoxy.bottomsheet
import android.text.method.MovementMethod
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@ -27,6 +28,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.util.MatrixItem
/**
@ -44,6 +46,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
@EpoxyAttribute
lateinit var body: CharSequence
@EpoxyAttribute
var imageContentRenderer: ImageContentRenderer? = null
@EpoxyAttribute
var data: ImageContentRenderer.Data? = null
@EpoxyAttribute
var time: CharSequence? = null
@ -59,16 +67,26 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
holder.avatar.setOnClickListener { userClicked?.invoke() }
holder.sender.setOnClickListener { userClicked?.invoke() }
holder.sender.setTextOrHide(matrixItem.displayName)
data?.let {
imageContentRenderer?.render(it, ImageContentRenderer.Mode.THUMBNAIL, holder.imagePreview)
}
holder.imagePreview.isVisible = data != null
holder.body.movementMethod = movementMethod
holder.body.text = body
body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time)
}
override fun unbind(holder: Holder) {
imageContentRenderer?.clear(holder.imagePreview)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val avatar by bind<ImageView>(R.id.bottom_sheet_message_preview_avatar)
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
val imagePreview by bind<ImageView>(R.id.bottom_sheet_message_preview_image)
}
}

View file

@ -16,62 +16,7 @@
package im.vector.app.core.utils
import java.util.regex.Pattern
private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
"|[\uD83E\uDD00-\uD83E\uDDFF]" +
"|[\uD83D\uDE00-\uD83D\uDE4F]" +
"|[\uD83D\uDE80-\uD83D\uDEFF]" +
"|[\u2600-\u26FF]\uFE0F?" +
"|[\u2700-\u27BF]\uFE0F?" +
"|\u24C2\uFE0F?" +
"|[\uD83C\uDDE6-\uD83C\uDDFF]{1,2}" +
"|[\uD83C\uDD70\uD83C\uDD71\uD83C\uDD7E\uD83C\uDD7F\uD83C\uDD8E\uD83C\uDD91-\uD83C\uDD9A]\uFE0F?" +
"|[\u0023\u002A\u0030-\u0039]\uFE0F?\u20E3" +
"|[\u2194-\u2199\u21A9-\u21AA]\uFE0F?" +
"|[\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55]\uFE0F?" +
"|[\u2934\u2935]\uFE0F?" +
"|[\u3030\u303D]\uFE0F?" +
"|[\u3297\u3299]\uFE0F?" +
"|[\uD83C\uDE01\uD83C\uDE02\uD83C\uDE1A\uD83C\uDE2F\uD83C\uDE32-\uD83C\uDE3A\uD83C\uDE50\uD83C\uDE51]\uFE0F?" +
"|[\u203C\u2049]\uFE0F?" +
"|[\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE]\uFE0F?" +
"|[\u00A9\u00AE]\uFE0F?" +
"|[\u2122\u2139]\uFE0F?" +
"|\uD83C\uDC04\uFE0F?" +
"|\uD83C\uDCCF\uFE0F?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
/*
// A hashset from all supported emoji
private var knownEmojiSet: HashSet<String>? = null
fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
GlobalScope.launch {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() }
val source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>().also {
source?.emojis?.mapTo(it) { (_, value) ->
value.emojiString()
}
}
done?.invoke()
}
}
}
fun isSingleEmoji(string: String): Boolean {
if (knownEmojiSet == null) {
Timber.e("Known Emoji Hashset not initialized")
// use fallback regexp
return containsOnlyEmojis(string)
}
return knownEmojiSet?.contains(string) ?: false
}
*/
import com.vanniktech.emoji.EmojiUtils
/**
* Test if a string contains emojis.
@ -82,36 +27,8 @@ fun isSingleEmoji(string: String): Boolean {
* @return true if the body contains only emojis
*/
fun containsOnlyEmojis(str: String?): Boolean {
var res = false
if (str != null && str.isNotEmpty()) {
val matcher = emojisPattern.matcher(str)
var start = -1
var end = -1
while (matcher.find()) {
val nextStart = matcher.start()
// first emoji position
if (start < 0) {
if (nextStart > 0) {
return false
}
} else {
// must not have a character between
if (nextStart != end) {
return false
}
}
start = nextStart
end = matcher.end()
}
res = -1 != start && end == str.length
}
return res
// Now rely on vanniktech library
return EmojiUtils.isOnlyEmojis(str)
}
/**

View file

@ -43,6 +43,7 @@ import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository
import kotlinx.parcelize.Parcelize
import kotlinx.coroutines.Dispatchers
@ -83,6 +84,8 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
override fun getBinding() = ActivityMainBinding.inflate(layoutInflater)
override fun getOtherThemes() = ActivityOtherThemes.Launcher
private lateinit var args: MainActivityArgs
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager

View file

@ -30,10 +30,8 @@ import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -59,8 +57,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
key
)
.toSpannable()
.colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
.colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
// TODO Restore coloration when we will have a FAQ to open with those terms
// .colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
// .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
views.ssssPassphraseEnterEdittext.editorActionEvents()
.throttleFirst(300, TimeUnit.MILLISECONDS)

View file

@ -119,13 +119,18 @@ class IncomingVerificationRequestHandler @Inject constructor(
Timber.v("## SAS verificationRequestCreated ${pr.transactionId}")
// For incoming request we should prompt (if not in activity where this request apply)
if (pr.isIncoming) {
val user = session?.getUser(pr.otherUserId)
val user = session?.getUser(pr.otherUserId)?.toMatrixItem()
val name = user?.getBestName() ?: pr.otherUserId
val description = if (name == pr.otherUserId) {
name
} else {
"$name (${pr.otherUserId})"
}
val alert = VerificationVectorAlert(
uniqueIdForVerificationRequest(pr),
context.getString(R.string.sas_incoming_request_notif_title),
"$name(${pr.otherUserId})",
description,
R.drawable.ic_shield_black,
shouldBeDisplayedIn = { activity ->
if (activity is RoomDetailActivity) {
@ -136,7 +141,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
}
)
.apply {
viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get())
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get())
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
val roomId = pr.roomId

View file

@ -58,7 +58,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
data class CancelSend(val eventId: String) : RoomDetailAction()
data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction()
data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()

View file

@ -98,6 +98,7 @@ import im.vector.app.core.ui.views.FailedMessagesWarningView
import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.KeyboardStateUtils
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.app.core.utils.TextUtils
@ -137,6 +138,7 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
@ -223,6 +225,7 @@ class RoomDetailFragment @Inject constructor(
private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val notificationUtils: NotificationUtils,
private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer,
@ -873,6 +876,15 @@ class RoomDetailFragment @Inject constructor(
}
views.composerLayout.views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
// Image Event
val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
val isImageVisible = if (data != null) {
imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerLayout.views.composerRelatedMessageImage)
true
} else {
false
}
updateComposerText(defaultContent)
views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
@ -884,6 +896,7 @@ class RoomDetailFragment @Inject constructor(
if (isAdded) {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
}
}
focusComposerAndShowKeyboard()
@ -1570,14 +1583,18 @@ class RoomDetailFragment @Inject constructor(
}
private fun handleCancelSend(action: EventSharedAction.Cancel) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
}
.show()
if (action.force) {
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true))
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false))
}
.show()
}
}
override fun onAvatarClicked(informationData: MessageInformationData) {

View file

@ -1208,6 +1208,10 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleCancel(action: RoomDetailAction.CancelSend) {
if (action.force) {
room.cancelSend(action.eventId)
return
}
val targetEventId = action.eventId
room.getTimeLineEvent(targetEventId)?.let {
// State must be in one of the sending states

View file

@ -63,7 +63,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
data class Redact(val eventId: String, val askForReason: Boolean) :
EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)
data class Cancel(val eventId: String) :
data class Cancel(val eventId: String, val force: Boolean) :
EventSharedAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) :

View file

@ -29,11 +29,14 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.send.SendState
import javax.inject.Inject
@ -45,6 +48,8 @@ class MessageActionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider,
private val imageContentRenderer: ImageContentRenderer,
private val dimensionConverter: DimensionConverter,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() {
@ -59,6 +64,8 @@ class MessageActionsEpoxyController @Inject constructor(
avatarRenderer(avatarRenderer)
matrixItem(state.informationData.matrixItem)
movementMethod(createLinkMovementMethod(listener))
imageContentRenderer(imageContentRenderer)
data(state.timelineEvent()?.buildImageContentRendererData(dimensionConverter.dpToPx(66)))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(state.messageBody.linkify(listener))
time(formattedDate)

View file

@ -250,6 +250,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
timelineEvent.root.sendState == SendState.SYNCED -> {
addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType)
}
timelineEvent.root.sendState == SendState.SENT -> {
addActionsForSentNotSyncedState(timelineEvent)
}
}
}
}
@ -287,10 +290,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
// TODO is uploading attachment?
if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(timelineEvent.eventId))
add(EventSharedAction.Cancel(timelineEvent.eventId, false))
}
}
private fun ArrayList<EventSharedAction>.addActionsForSentNotSyncedState(timelineEvent: TimelineEvent) {
// If sent but not synced (synapse stuck at bottom bug)
// Still offer action to cancel (will only remove local echo)
timelineEvent.root.eventId?.let {
add(EventSharedAction.Cancel(it, true))
}
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
private fun ArrayList<EventSharedAction>.addActionsForSyncedState(timelineEvent: TimelineEvent,
actionPermissions: ActionPermissions,
messageContent: MessageContent?,
@ -337,12 +352,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
}
if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
if (vectorPreferences.developerMode()) {

View file

@ -64,7 +64,6 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.REACTION,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback)
@ -91,6 +90,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.REACTION,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.image
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
fun TimelineEvent.buildImageContentRendererData(maxHeight: Int): ImageContentRenderer.Data? {
return when {
root.isImageMessage() -> root.getClearContent().toModel<MessageImageContent>()
?.let { messageImageContent ->
ImageContentRenderer.Data(
eventId = eventId,
filename = messageImageContent.body,
mimeType = messageImageContent.mimeType,
url = messageImageContent.getFileUrl(),
elementToDecrypt = messageImageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageImageContent.info?.height,
maxHeight = maxHeight,
width = messageImageContent.info?.width,
maxWidth = maxHeight * 2,
allowNonMxcUrls = false
)
}
root.isVideoMessage() -> root.getClearContent().toModel<MessageVideoContent>()
?.let { messageVideoContent ->
ImageContentRenderer.Data(
eventId = eventId,
filename = messageVideoContent.body,
mimeType = messageVideoContent.mimeType,
url = messageVideoContent.getFileUrl(),
elementToDecrypt = messageVideoContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageVideoContent.videoInfo?.height,
maxHeight = maxHeight,
width = messageVideoContent.videoInfo?.width,
maxWidth = maxHeight * 2,
allowNonMxcUrls = false
)
}
else -> null
}
}

View file

@ -52,7 +52,7 @@ class PreviewUrlRetriever(session: Session,
// The event is not known or it has been edited
// Keep only the first URL for the moment
val url = mediaService.extractUrls(event)
.firstOrNull()
.firstOrNull { canShowUrlPreview(it) }
?.takeIf { it !in blockedUrl }
if (url == null) {
updateState(eventId, latestEventId, PreviewUrlUiState.NoUrl)
@ -98,6 +98,10 @@ class PreviewUrlRetriever(session: Session,
}
}
private fun canShowUrlPreview(url: String): Boolean {
return blockedDomains.all { !url.startsWith(it) }
}
fun doNotShowPreviewUrlFor(eventId: String, url: String) {
blockedUrl.add(url)
@ -143,5 +147,12 @@ class PreviewUrlRetriever(session: Session,
companion object {
// One week in millis
private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000
private val blockedDomains = listOf(
"https://matrix.to",
"https://app.element.io",
"https://staging.element.io",
"https://develop.element.io"
)
}
}

View file

@ -92,7 +92,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
return RoomSummaryItem_()
.id(roomSummary.roomId)
.avatarRenderer(avatarRenderer)
.encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
// We do not display shield in the room list anymore
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
.matrixItem(roomSummary.toMatrixItem())
.lastEventTime(latestEventTime)
.typingMessage(typingMessage)

View file

@ -16,18 +16,24 @@
package im.vector.app.features.login
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.databinding.FragmentLoginSplashBinding
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
/**
* In this screen, the user is viewing an introduction to what he can do with this application
*/
class LoginSplashFragment @Inject constructor() : AbstractLoginFragment<FragmentLoginSplashBinding>() {
class LoginSplashFragment @Inject constructor(
private val vectorPreferences: VectorPreferences
) : AbstractLoginFragment<FragmentLoginSplashBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding {
return FragmentLoginSplashBinding.inflate(inflater, container, false)
@ -41,6 +47,14 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment<Fragment
private fun setupViews() {
views.loginSplashSubmit.setOnClickListener { getStarted() }
if (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
views.loginSplashVersion.isVisible = true
@SuppressLint("SetTextI18n")
views.loginSplashVersion.text = "Version : ${BuildConfig.VERSION_NAME}\n" +
"Branch: ${BuildConfig.GIT_BRANCH_NAME}\n" +
"Build: ${BuildConfig.BUILD_NUMBER}"
}
}
private fun getStarted() {

View file

@ -29,7 +29,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@ -132,7 +131,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
if (savedInstanceState == null) {
pager2.setCurrentItem(initialIndex, false)
// The page change listener is not notified of the change...
lifecycleScope.launchWhenResumed {
pager2.post {
onSelectedPositionChanged(initialIndex)
}
}

View file

@ -26,6 +26,7 @@ import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.isAnimationDisabled
import im.vector.app.features.pin.PinActivity
import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.themes.ThemeUtils
import timber.log.Timber
import java.lang.ref.WeakReference
@ -294,6 +295,7 @@ class PopupAlertManager @Inject constructor() {
private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean {
return alert != null
&& activity !is PinActivity
&& activity !is SignedOutActivity
&& activity is VectorBaseActivity<*>
&& alert.shouldBeDisplayedIn.invoke(activity)
}

View file

@ -222,7 +222,6 @@ class RoomProfileController @Inject constructor(
buildProfileAction(
id = "devTools",
title = stringProvider.getString(R.string.dev_tools_menu_name),
subtitle = roomSummary.roomId,
dividerColor = dividerColor,
divider = false,
editable = true,

View file

@ -128,6 +128,7 @@ class RoomAliasFragment @Inject constructor(
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
}

View file

@ -117,6 +117,7 @@ class RoomBannedMemberListFragment @Inject constructor(
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
}
}

View file

@ -140,6 +140,7 @@ class RoomMemberListFragment @Inject constructor(
state.roomSummary()?.let {
views.roomSettingGeneric.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingGeneric.roomSettingsToolbarAvatarImageView)
views.roomSettingGeneric.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
}
}

View file

@ -91,6 +91,7 @@ class RoomPermissionsFragment @Inject constructor(
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
}

View file

@ -154,6 +154,7 @@ class RoomSettingsFragment @Inject constructor(
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
invalidateOptionsMenu()

View file

@ -31,6 +31,11 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
R.style.AppTheme_Black
)
object Launcher : ActivityOtherThemes(
R.style.AppTheme_Launcher,
R.style.AppTheme_Launcher
)
object AttachmentsPreview : ActivityOtherThemes(
R.style.AppTheme_AttachmentsPreview,
R.style.AppTheme_AttachmentsPreview

View file

@ -68,6 +68,13 @@
app:tint="?riotx_text_primary"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/composerRelatedMessageImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
tools:ignore="MissingPrefix" />
<ImageButton
android:id="@+id/composerRelatedMessageCloseButton"
android:layout_width="22dp"

View file

@ -83,6 +83,15 @@
tools:ignore="MissingConstraints,MissingPrefix"
tools:src="@drawable/ic_edit" />
<ImageView
android:id="@+id/composerRelatedMessageImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toEndOf="parent"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageButton
android:id="@+id/composerRelatedMessageCloseButton"

View file

@ -60,6 +60,20 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/first_names" />
<ImageView
android:id="@+id/composerRelatedMessageImage"
android:layout_width="100dp"
android:layout_height="66dp"
android:layout_marginTop="6dp"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/composerRelatedMessageTitle"
app:layout_constraintTop_toBottomOf="@+id/composerRelatedMessageTitle"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/backgrounds/scenic"
tools:visibility="visible" />
<TextView
android:id="@+id/composerRelatedMessageContent"
android:layout_width="0dp"
@ -70,7 +84,7 @@
app:layout_constrainedHeight="true"
app:layout_constraintEnd_toEndOf="@id/composerRelatedMessageTitle"
app:layout_constraintStart_toStartOf="@id/composerRelatedMessageTitle"
app:layout_constraintTop_toBottomOf="@id/composerRelatedMessageTitle"
app:layout_constraintTop_toBottomOf="@id/composerRelatedMessageImage"
tools:text="@tools:sample/lorem/random" />
<ImageView
@ -89,7 +103,6 @@
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_edit" />
<ImageButton
android:id="@+id/composerRelatedMessageCloseButton"
android:layout_width="48dp"
@ -97,9 +110,9 @@
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/cancel"
android:src="@drawable/ic_close_round"
app:layout_constraintBottom_toBottomOf="@id/composerRelatedMessageContent"
app:layout_constraintBottom_toBottomOf="@id/composer_preview_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/composerRelatedMessageContent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/riotx_notice"
tools:ignore="MissingPrefix" />

View file

@ -193,4 +193,15 @@
app:layout_constraintTop_toBottomOf="@+id/loginSplashSubmit"
app:layout_constraintVertical_weight="4" />
<TextView
android:id="@+id/loginSplashVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?riotx_text_secondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/settings_version"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -34,6 +34,15 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/roomSettingsDecorationToolbarAvatarImageView"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintCircle="@+id/roomSettingsToolbarAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/roomSettingsToolbarTitleView"
android:layout_width="0dp"

View file

@ -8,7 +8,9 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingTop="32dp"
android:paddingBottom="32dp">
<ImageView
android:id="@+id/ssss_shield"
@ -28,7 +30,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="16dp"
android:text="@string/recovery_passphrase"
android:textColor="?riotx_text_primary"
@ -38,7 +39,6 @@
app:layout_constraintStart_toEndOf="@id/ssss_shield"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssss_restore_with_passphrase_warning_text"
android:layout_width="0dp"
@ -51,7 +51,6 @@
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase"
tools:text="@string/enter_secret_storage_passphrase_or_key" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssss_passphrase_enter_til"
style="@style/VectorTextInputLayout"
@ -82,61 +81,62 @@
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til"
app:tint="?colorAccent" />
<!-- -->
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_use_key"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_recovery_key"
app:icon="@drawable/ic_security_key_24dp"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_submit"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/_continue"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_passphrase_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
android:text="@string/_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
<TextView
android:id="@+id/ssss_passphrase_or"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/or"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_submit" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_use_key"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/use_recovery_key"
app:icon="@drawable/ic_security_key_24dp"
app:layout_constraintBottom_toTopOf="@+id/ssss_passphrase_reset"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til"
app:layout_goneMarginBottom="32dp" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_or" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/ssss_passphrase_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
android:clickable="true"
android:focusable="true"
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_flow"
app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_use_key"
app:leftIcon="@drawable/ic_alert_triangle"
app:tint="@color/vector_error_color"
app:titleTextColor="?riotx_text_secondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -50,6 +50,20 @@
app:layout_constraintEnd_toEndOf="parent"
tools:text="Friday 8pm" />
<ImageView
android:id="@+id/bottom_sheet_message_preview_image"
android:layout_width="100dp"
android:layout_height="66dp"
android:layout_marginTop="6dp"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/bottom_sheet_message_preview_sender"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/backgrounds/scenic"
tools:visibility="visible" />
<TextView
android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp"
@ -65,7 +79,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_image"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="8dp">
<View
@ -12,16 +11,16 @@
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="?riotx_header_panel_background" />
android:background="?riotx_list_bottom_sheet_divider_color" />
<TextView
android:id="@+id/itemDayTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?riotx_background"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:background="?android:attr/windowBackground"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:textColor="?riotx_header_panel_text_secondary"
android:textSize="15sp"
tools:text="@tools:sample/date/day_of_week" />

View file

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:orientation="vertical">
<TextView

View file

@ -2,7 +2,6 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="8dp">
<View
@ -17,10 +16,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?riotx_background"
android:background="?android:attr/windowBackground"
android:fontFamily="sans-serif-medium"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/timeline_unread_messages"
android:textColor="@color/notification_accent_color"
android:textSize="15sp" />