Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
7453509df4
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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]
|
||||
|
||||
|
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
||||
|
47
CHANGES.md
47
CHANGES.md
@ -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
|
||||
|
||||
|
@ -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'
|
||||
}
|
@ -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'
|
||||
|
@ -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.
|
||||
|
||||
|
2
fastlane/metadata/android/en-US/changelogs/40101020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40101020.txt
Normal 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
|
2
fastlane/metadata/android/en-US/changelogs/40101030.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40101030.txt
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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"]) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>) {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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 -> {
|
||||
|
@ -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()
|
||||
|
@ -37,4 +37,7 @@ internal abstract class SyncModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindRoomSyncEphemeralTemporaryStore(store: RoomSyncEphemeralTemporaryStoreFile): RoomSyncEphemeralTemporaryStore
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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>()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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) :
|
||||
|
@ -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)
|
||||
|
@ -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()) {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user