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

This commit is contained in:
Damir Jelić 2021-11-29 17:43:40 +01:00
commit 2167564812
170 changed files with 2763 additions and 1675 deletions

View File

@ -56,10 +56,10 @@ jobs:
java-version: '11' java-version: '11'
- name: Run sanity tests on API ${{ matrix.api-level }} - name: Run sanity tests on API ${{ matrix.api-level }}
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true # allow pipeline to upload failure results
with: with:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
profile: 24 # Pixel 5
emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160 emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
script: | script: |
adb root adb root
@ -67,11 +67,10 @@ jobs:
touch emulator.log touch emulator.log
chmod 777 emulator.log chmod 777 emulator.log
adb logcat >> emulator.log & adb logcat >> emulator.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1
- name: Upload Failing Test Report Log - name: Upload Failing Test Report Log
if: failure()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
if: failure()
with: with:
name: sanity-error-results name: sanity-error-results
path: | path: |

View File

@ -7,6 +7,8 @@ on:
jobs: jobs:
sync-emojis: sync-emojis:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.8 - name: Set up Python 3.8
@ -39,6 +41,8 @@ jobs:
sync-sas-strings: sync-sas-strings:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.8 - name: Set up Python 3.8

View File

@ -1,3 +1,35 @@
Changes in Element v1.3.8 (2021-11-17)
======================================
Features ✨
----------
- Android 12 support ([#4433](https://github.com/vector-im/element-android/issues/4433))
- Make notification text spoiler aware ([#3477](https://github.com/vector-im/element-android/issues/3477))
- Poll Feature - Create Poll Screen (Disabled for now) ([#4367](https://github.com/vector-im/element-android/issues/4367))
- Adds support for images inside message notifications ([#4402](https://github.com/vector-im/element-android/issues/4402))
Bugfixes 🐛
----------
- Render markdown in room list ([#452](https://github.com/vector-im/element-android/issues/452))
- Fix incorrect cropping of conversation icons ([#4424](https://github.com/vector-im/element-android/issues/4424))
- Fix potential NullPointerException crashes in Room and User account data sources ([#4428](https://github.com/vector-im/element-android/issues/4428))
- Unable to establish Olm outbound session from fallback key ([#4446](https://github.com/vector-im/element-android/issues/4446))
- Fixes intermittent crash on sign out due to the session being incorrectly recreated whilst being closed ([#4480](https://github.com/vector-im/element-android/issues/4480))
SDK API changes ⚠️
------------------
- Add content scanner API from MSC1453
API documentation : https://github.com/matrix-org/matrix-content-scanner#api ([#4392](https://github.com/vector-im/element-android/issues/4392))
- Breaking SDK API change to PushRuleListener, the separated callbacks have been merged into one with a data class which includes all the previously separated push information ([#4401](https://github.com/vector-im/element-android/issues/4401))
Other changes
-------------
- Finish migration from RxJava to Flow ([#4219](https://github.com/vector-im/element-android/issues/4219))
- Remove redundant text in feature request issue form ([#4257](https://github.com/vector-im/element-android/issues/4257))
- Add and improve issue triage workflows ([#4435](https://github.com/vector-im/element-android/issues/4435))
- Update issue template to bring in line with element-web ([#4452](https://github.com/vector-im/element-android/issues/4452))
Changes in Element v1.3.7 (2021-11-04) Changes in Element v1.3.7 (2021-11-04)
====================================== ======================================

View File

@ -27,6 +27,20 @@ At each Element release, the SDK module is copied to a dedicated repository: htt
The version 1.0.0 of Element still misses some features which was previously included in Riot-Android. The version 1.0.0 of Element still misses some features which was previously included in Riot-Android.
The team will work to add them on a regular basis. The team will work to add them on a regular basis.
# Releases to app stores
There is some delay between when a release is created and when it appears in the app stores (Google Play Store and F-Droid). Here are some of the reasons:
* Not all versioned releases that appear on GitHub are considered stable. Each release is first considered beta: this continues for at least two days. If the release is stable (no serious issues or crashes are reported), then it is released as a production release in Google Play Store, and a request is sent to F-Droid too.
* Each release on the Google Play Store undergoes review by Google before it comes out. This can take an unpredictable amount of time. In some cases it has taken several weeks.
* In order for F-Droid to guarantee that the app you receive exactly matches the public source code, they build releases themselves. When a release is considered stable, Element staff inform the F-Droid maintainers and it is added to the build queue. Depending on the load on F-Droid's infrastructure, it can take some time for releases to be built. This always takes at least 24 hours, and can take several days.
If you would like to receive releases more quickly (bearing in mind that they may not be stable) you have a number of options:
1. [Sign up to receive beta releases](https://play.google.com/apps/testing/im.vector.app) via the Google Play Store.
2. Install a [release APK](https://github.com/vector-im/element-android/releases) directly - download the relevant .apk file and allow installing from untrusted sources in your device settings. Note: these releases are the Google Play version, which depend on some Google services. If you prefer to avoid that, try the latest dev builds, and choose the F-Droid version.
3. If you're really brave, install the [very latest dev build](https://buildkite.com/matrix-dot-org/element-android/builds/latest?branch=develop&state=passed) - click on *Assemble (GPlay or FDroid) Debug version* then on *Artifacts*.
## Contributing ## Contributing
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!

View File

@ -69,9 +69,9 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
// Jitsi repo // Jitsi repo
maven { maven {
url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-3.1.0" url "https://github.com/vector-im/jitsi_libre_maven/raw/main/android-sdk-3.10.0"
// Note: to test Jitsi release you can use a local file like this: // Note: to test Jitsi release you can use a local file like this:
// url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-3.1.0" // url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-3.10.0"
} }
google() google()
mavenCentral() mavenCentral()

1
changelog.d/3449.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes left over text when inserting emojis via the ':' menu and replaces the last typed ':' rather than the one at the end of the message

View File

@ -1 +0,0 @@
Make notification text spoiler aware

1
changelog.d/3833.bugfix Normal file
View File

@ -0,0 +1 @@
Fixing queued voice message failing to send or retry

1
changelog.d/4022.bugfix Normal file
View File

@ -0,0 +1 @@
Keeping device screen on whilst recording and playing back voice messages

1
changelog.d/4067.bugfix Normal file
View File

@ -0,0 +1 @@
Allow voice messages to continue recording during device rotation

1
changelog.d/4144.bugfix Normal file
View File

@ -0,0 +1 @@
Allowing users to hang up VOIP calls during the initialisation phase (avoids getting stuck in the call screen if something goes wrong)

View File

@ -1 +0,0 @@
Finish migration from RxJava to Flow

1
changelog.d/4246.feature Normal file
View File

@ -0,0 +1 @@
Make Element Android Thread aware

View File

@ -1 +0,0 @@
Remove redundant text in feature request issue form

1
changelog.d/4338.bugfix Normal file
View File

@ -0,0 +1 @@
Make the verification shields the same in Element Web and Element Android

1
changelog.d/4343.bugfix Normal file
View File

@ -0,0 +1 @@
Fix a display issue in the composer when the replied message is changed.

View File

@ -1 +0,0 @@
Poll Feature - Create Poll Screen (Disabled for now)

View File

@ -1,2 +0,0 @@
Add content scanner API from MSC1453
API documentation : https://github.com/matrix-org/matrix-content-scanner#api

View File

@ -1 +0,0 @@
Breaking SDK API change to PushRuleListener, the separated callbacks have been merged into one with a data class which includes all the previously separated push information

View File

@ -1 +0,0 @@
Adds support for images inside message notifications

View File

@ -1 +0,0 @@
Fix incorrect cropping of conversation icons

View File

@ -1 +0,0 @@
Fix potential NullPointerException crashes in Room and User account data sources

View File

@ -1 +0,0 @@
Add and improve issue triage workflows

View File

@ -1 +0,0 @@
Unable to establish Olm outbound session from fallback key

View File

@ -1 +0,0 @@
Update issue template to bring in line with element-web

View File

@ -1 +0,0 @@
Fixes intermittent crash on sign out due to the session being incorrectly recreated whilst being closed

1
changelog.d/4488.bugfix Normal file
View File

@ -0,0 +1 @@
Dismissing the Fdroid variant Listening for notifications on sign out, fixes crash when tapping the notification when signed out

1
changelog.d/4504.misc Normal file
View File

@ -0,0 +1 @@
Upgrade Jitsi lib (and so webrtc) from Jitsi android-sdk-3.1.0 to android-sdk-3.10.0

1
changelog.d/4507.misc Normal file
View File

@ -0,0 +1 @@
Improve crypto logs to help debug decryption failures

1
changelog.d/4515.misc Normal file
View File

@ -0,0 +1 @@
Voice recording mic button refactor with small animation tweaks in preparation for voice drafts

View File

@ -1 +0,0 @@
Render markdown in room list

1
changelog.d/4520.bugfix Normal file
View File

@ -0,0 +1 @@
Fix a crash when displaying the bootstrap bottom sheet

1
changelog.d/4539.bugfix Normal file
View File

@ -0,0 +1 @@
Remove duplicated settings declaration

1
changelog.d/4552.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes .ogg files failing to upload to rooms

View File

@ -11,7 +11,7 @@ def gradle = "7.0.3"
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
def kotlin = "1.5.31" def kotlin = "1.5.31"
def kotlinCoroutines = "1.5.2" def kotlinCoroutines = "1.5.2"
def dagger = "2.40.1" def dagger = "2.40.2"
def retrofit = "2.9.0" def retrofit = "2.9.0"
def arrow = "0.8.2" def arrow = "0.8.2"
def markwon = "4.6.2" def markwon = "4.6.2"
@ -45,13 +45,13 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
], ],
androidx : [ androidx : [
'appCompat' : "androidx.appcompat:appcompat:1.3.1", 'appCompat' : "androidx.appcompat:appcompat:1.4.0",
'core' : "androidx.core:core-ktx:1.7.0", 'core' : "androidx.core:core-ktx:1.7.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.0",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.2",
'work' : "androidx.work:work-runtime-ktx:2.7.0", 'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0", 'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
'junit' : "androidx.test.ext:junit:1.1.3", 'junit' : "androidx.test.ext:junit:1.1.3",

View File

@ -18,7 +18,7 @@ The generated maven repository is then host in the project https://github.com/ve
Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`.
Currently we are building the version with the tag `android-sdk-3.1.0`. Currently we are building the version with the tag `android-sdk-3.10.0`.
### Run the build script ### Run the build script
@ -35,7 +35,7 @@ It will build the Jitsi Meet Android library and put every generated files in th
- Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line: - Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line:
```groovy ```groovy
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0" url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0"
``` ```
You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository. You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository.
@ -43,13 +43,13 @@ You can uncomment and update the line starting with `// url "file://...` and com
- Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line: - Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line:
```groovy ```groovy
implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') implementation('org.jitsi.react:jitsi-meet-sdk:3.10.0')
``` ```
- Update the dependency of the WebRTC library in the file `./vector/build.gradle`. Currently we have this line: - Update the dependency of the WebRTC library in the file `./vector/build.gradle`. Currently we have this line:
```groovy ```groovy
implementation('com.facebook.react:react-native-webrtc:1.87.3-jitsi-6624067@aar') implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar')
``` ```
- Perform a gradle sync and build the project - Perform a gradle sync and build the project
@ -74,7 +74,7 @@ If all the tests are passed, you can export the generated Jitsi library to our M
- Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line: - Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line:
```groovy ```groovy
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0" url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0"
``` ```
- Build the project and perform the sanity tests again. - Build the project and perform the sanity tests again.

View File

@ -1,2 +1,2 @@
Main changes in this version: Bug fixes mainly regarding the notifications. Main changes in this version: Bug fixes mainly regarding the notifications.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7 Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2

View File

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

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#DDD"
android:orientation="vertical"
android:padding="16dp"
tools:ignore="HardcodedText">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Light" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/element_background_light"
android:orientation="vertical"
android:padding="16dp">
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Facebook.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Github.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Apple.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Dark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/element_background_dark"
android:orientation="vertical"
android:padding="16dp">
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Facebook.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Github.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Apple.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
<Button
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue with XXX" />
</LinearLayout>
</LinearLayout>

View File

@ -9,7 +9,7 @@
<item name="iconGravity">start</item> <item name="iconGravity">start</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:textAlignment">center</item> <item name="android:textAlignment">center</item>
<item name="android:paddingStart">2dp</item> <item name="android:paddingStart">4dp</item>
<!-- Compensate icon size to center text correctly--> <!-- Compensate icon size to center text correctly-->
<item name="android:paddingEnd">38dp</item> <item name="android:paddingEnd">38dp</item>
<item name="android:clipToPadding">false</item> <item name="android:clipToPadding">false</item>

View File

@ -129,7 +129,9 @@
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item> <item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item> <item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
<item name="vctr_social_login_button_google_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Dark</item> <!-- For Google button style, use same values than for light theme, for a better rendering (white background)
see https://github.com/vector-im/element-android/issues/4285#issuecomment-974270998 -->
<item name="vctr_social_login_button_google_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Light</item>
<item name="vctr_social_login_button_github_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Github.Dark</item> <item name="vctr_social_login_button_github_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Github.Dark</item>
<item name="vctr_social_login_button_facebook_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Facebook.Dark</item> <item name="vctr_social_login_button_facebook_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Facebook.Dark</item>
<item name="vctr_social_login_button_twitter_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Dark</item> <item name="vctr_social_login_button_twitter_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Twitter.Dark</item>

View File

@ -31,7 +31,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.3.8\"" buildConfigField "String", "SDK_VERSION", "\"1.3.9\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
@ -131,7 +131,7 @@ dependencies {
implementation libs.squareup.retrofit implementation libs.squareup.retrofit
implementation libs.squareup.retrofitMoshi implementation libs.squareup.retrofitMoshi
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.2")) implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.okhttp3:logging-interceptor'
implementation 'com.squareup.okhttp3:okhttp-urlconnection' implementation 'com.squareup.okhttp3:okhttp-urlconnection'
@ -174,10 +174,10 @@ dependencies {
implementation libs.apache.commonsImaging implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.38'
testImplementation libs.tests.junit testImplementation libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7' testImplementation 'org.robolectric:robolectric:4.7.2'
//testImplementation 'org.robolectric:shadows-support-v4:3.0' //testImplementation 'org.robolectric:shadows-support-v4:3.0'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation libs.mockk.mockk testImplementation libs.mockk.mockk

View File

@ -20,7 +20,18 @@ package org.matrix.android.sdk.api
* This class contains pattern to match Matrix Url, aka mxc urls * This class contains pattern to match Matrix Url, aka mxc urls
*/ */
object MatrixUrls { object MatrixUrls {
/**
* "mxc" scheme, including "://". So "mxc://"
*/
const val MATRIX_CONTENT_URI_SCHEME = "mxc://" const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
/**
* Return true if the String starts with "mxc://"
*/
fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME) fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME)
/**
* Remove the "mxc://" prefix. No op if the String is not a Mxc URL
*/
fun String.removeMxcPrefix() = removePrefix(MATRIX_CONTENT_URI_SCHEME)
} }

View File

@ -26,6 +26,7 @@ open class LoggerTag(_value: String, parentTag: LoggerTag? = null) {
object SYNC : LoggerTag("SYNC") object SYNC : LoggerTag("SYNC")
object VOIP : LoggerTag("VOIP") object VOIP : LoggerTag("VOIP")
object CRYPTO : LoggerTag("CRYPTO")
val value: String = if (parentTag == null) { val value: String = if (parentTag == null) {
_value _value

View File

@ -44,7 +44,8 @@ data class ContentAttachmentData(
FILE, FILE,
IMAGE, IMAGE,
AUDIO, AUDIO,
VIDEO VIDEO,
VOICE_MESSAGE
} }
fun getSafeMimeType() = mimeType?.normalizeMimeType() fun getSafeMimeType() = mimeType?.normalizeMimeType()

View File

@ -28,6 +28,10 @@ object RelationType {
/** Lets you define an event which references an existing event.*/ /** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference" const val REFERENCE = "m.reference"
/** Lets you define an thread event that belongs to another existing event.*/
// const val THREAD = "m.thread" // m.thread is not yet released in the backend
const val THREAD = "io.element.thread" // io.element.thread will be replaced by m.thread when it is released
/** Lets you define an event which adds a response to an existing event.*/ /** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "org.matrix.response" const val RESPONSE = "org.matrix.response"
} }

View File

@ -23,15 +23,15 @@ package org.matrix.android.sdk.api.session.room.send
* EDIT: draft of an edition of a message * EDIT: draft of an edition of a message
* REPLY: draft of a reply of another message * REPLY: draft of a reply of another message
*/ */
sealed class UserDraft(open val text: String) { sealed interface UserDraft {
data class REGULAR(override val text: String) : UserDraft(text) data class Regular(val text: String) : UserDraft
data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) data class Quote(val linkedEventId: String, val text: String) : UserDraft
data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) data class Edit(val linkedEventId: String, val text: String) : UserDraft
data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) data class Reply(val linkedEventId: String, val text: String) : UserDraft
fun isValid(): Boolean { fun isValid(): Boolean {
return when (this) { return when (this) {
is REGULAR -> text.isNotBlank() is Regular -> text.isNotBlank()
else -> true else -> true
} }
} }

View File

@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@ -100,6 +101,9 @@ import kotlin.math.max
* CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto.
* Specially, it tracks all room membership changes events in order to do keys updates. * Specially, it tracks all room membership changes events in order to do keys updates.
*/ */
private val loggerTag = LoggerTag("DefaultCryptoService", LoggerTag.CRYPTO)
@SessionScope @SessionScope
internal class DefaultCryptoService @Inject constructor( internal class DefaultCryptoService @Inject constructor(
@UserId @UserId
@ -183,7 +187,7 @@ internal class DefaultCryptoService @Inject constructor(
try { try {
downloadKeys(listOf(userId), true) downloadKeys(listOf(userId), true)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.w(failure, "setDeviceName: Failed to refresh of crypto device") Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device")
} }
} }
callback.onSuccess(data) callback.onSuccess(data)
@ -318,11 +322,11 @@ internal class DefaultCryptoService @Inject constructor(
try { try {
setRustLogger() setRustLogger()
Timber.v( Timber.tag(loggerTag.value).v(
"## CRYPTO | Successfully started up an Olm machine for " + "## CRYPTO | Successfully started up an Olm machine for " +
"$userId, $deviceId, identity keys: ${this.olmMachine.identityKeys()}") "$userId, $deviceId, identity keys: ${this.olmMachine.identityKeys()}")
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.v("Failed create an Olm machine: $throwable") Timber.tag(loggerTag.value).v("Failed create an Olm machine: $throwable")
} }
// We try to enable key backups, if the backup version on the server is trusted, // We try to enable key backups, if the backup version on the server is trusted,
@ -449,12 +453,12 @@ internal class DefaultCryptoService @Inject constructor(
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false return false
} }
if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) { if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") Timber.tag(loggerTag.value).e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
return false return false
} }
@ -466,7 +470,7 @@ internal class DefaultCryptoService @Inject constructor(
// e2e rooms with them, so there is room for optimisation here, but for now // e2e rooms with them, so there is room for optimisation here, but for now
// we just invalidate everyone in the room. // we just invalidate everyone in the room.
if (null == existingAlgorithm) { if (null == existingAlgorithm) {
Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") Timber.tag(loggerTag.value).d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein")
val userIds = ArrayList(membersId) val userIds = ArrayList(membersId)
olmMachine.updateTrackedUsers(userIds) olmMachine.updateTrackedUsers(userIds)
@ -531,16 +535,16 @@ internal class DefaultCryptoService @Inject constructor(
if (algorithm != null) { if (algorithm != null) {
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent() starts") Timber.tag(loggerTag.value).v("encryptEventContent() starts")
runCatching { runCatching {
preshareRoomKey(roomId, userIds) preshareRoomKey(roomId, userIds)
val content = encrypt(roomId, eventType, eventContent) val content = encrypt(roomId, eventType, eventContent)
Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
MXEncryptEventContentResult(content, EventType.ENCRYPTED) MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}.foldToCallback(callback) }.foldToCallback(callback)
} else { } else {
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## CRYPTO | encryptEventContent() : $reason") Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
} }
} }
@ -584,7 +588,7 @@ internal class DefaultCryptoService @Inject constructor(
private fun onRoomEncryptionEvent(roomId: String, event: Event) { private fun onRoomEncryptionEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) { if (!event.isStateEvent()) {
// Ignore // Ignore
Timber.w("Invalid encryption event") Timber.tag(loggerTag.value).w("Invalid encryption event")
return return
} }
@ -787,7 +791,7 @@ internal class DefaultCryptoService @Inject constructor(
} }
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") Timber.tag(loggerTag.value).e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
} }
} }
@ -1071,12 +1075,12 @@ internal class DefaultCryptoService @Inject constructor(
override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) { override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
// Ensure to load all room members // Ensure to load all room members
try { try {
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
callback.onFailure(failure) callback.onFailure(failure)
return@launch return@launch
} }
@ -1087,7 +1091,7 @@ internal class DefaultCryptoService @Inject constructor(
if (algorithm == null) { if (algorithm == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## CRYPTO | prepareToEncrypt() : $reason") Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason")
callback.onFailure(IllegalArgumentException("Missing algorithm")) callback.onFailure(IllegalArgumentException("Missing algorithm"))
return@launch return@launch
} }
@ -1097,7 +1101,7 @@ internal class DefaultCryptoService @Inject constructor(
}.fold( }.fold(
{ callback.onSuccess(Unit) }, { callback.onSuccess(Unit) },
{ {
Timber.e("## CRYPTO | prepareToEncrypt() failed.") Timber.tag(loggerTag.value).e(it, "prepareToEncrypt() failed.")
callback.onFailure(it) callback.onFailure(it)
} }
) )

View File

@ -16,17 +16,21 @@
package org.matrix.android.sdk.internal.crypto.actions package org.matrix.android.sdk.internal.crypto.actions
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXKey import org.matrix.android.sdk.internal.crypto.model.MXKey
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.toDebugString
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val ONE_TIME_KEYS_RETRY_COUNT = 3 private const val ONE_TIME_KEYS_RETRY_COUNT = 3
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
internal class EnsureOlmSessionsForDevicesAction @Inject constructor( internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
@ -36,15 +40,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
val results = MXUsersDevicesMap<MXOlmSessionResult>() val results = MXUsersDevicesMap<MXOlmSessionResult>()
for ((userId, deviceInfos) in devicesByUser) { for ((userId, deviceList) in devicesByUser) {
for (deviceInfo in deviceInfos) { for (deviceInfo in deviceList) {
val deviceId = deviceInfo.deviceId val deviceId = deviceInfo.deviceId
val key = deviceInfo.identityKey() val key = deviceInfo.identityKey()
if (key == null) {
Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key")
continue
}
val sessionId = olmDevice.getSessionId(key!!) val sessionId = olmDevice.getSessionId(key)
if (sessionId.isNullOrEmpty() || force) { if (sessionId.isNullOrEmpty() || force) {
Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)")
devicesWithoutSession.add(deviceInfo) devicesWithoutSession.add(deviceInfo)
} else {
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
} }
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
@ -52,6 +63,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
} }
} }
Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" +
" ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}")
if (devicesWithoutSession.size == 0) { if (devicesWithoutSession.size == 0) {
return results return results
} }
@ -71,13 +84,13 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
// //
// That should eventually resolve itself, but it's poor form. // That should eventually resolve itself, but it's poor form.
Timber.i("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}")
/* This is unused, the rust-sdk does things a bit differently. /* This is unused, the rust-sdk does things a bit differently.
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT)
Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
for ((userId, deviceInfos) in devicesByUser) { for ((userId, deviceInfos) in devicesByUser) {
for (deviceInfo in deviceInfos) { for (deviceInfo in deviceInfos) {
var oneTimeKey: MXKey? = null var oneTimeKey: MXKey? = null
@ -85,7 +98,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
if (null != deviceIds) { if (null != deviceIds) {
for (deviceId in deviceIds) { for (deviceId in deviceIds) {
val olmSessionResult = results.getObject(userId, deviceId) val olmSessionResult = results.getObject(userId, deviceId)
if (olmSessionResult!!.sessionId != null && !force) { if (olmSessionResult?.sessionId != null && !force) {
// We already have a result for this device // We already have a result for this device
continue continue
} }
@ -94,12 +107,11 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
oneTimeKey = key oneTimeKey = key
} }
if (oneTimeKey == null) { if (oneTimeKey == null) {
Timber.w("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId")
" for device " + userId + " : " + deviceId)
continue continue
} }
// Update the result for this device in results // Update the result for this device in results
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
} }
} }
} }
@ -116,31 +128,36 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
val signKeyId = "ed25519:$deviceId" val signKeyId = "ed25519:$deviceId"
val signature = oneTimeKey.signatureForUserId(userId, signKeyId) val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) { val fingerprint = deviceInfo.fingerprint()
if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) {
var isVerified = false var isVerified = false
var errorMessage: String? = null var errorMessage: String? = null
try { try {
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature)
isVerified = true isVerified = true
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(loggerTag.value).d(e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," +
" signature:$signature fingerprint:$fingerprint")
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " +
" - signable json ${oneTimeKey.signalableJSONDictionary()}")
errorMessage = e.message errorMessage = e.message
} }
// Check one-time key signature // Check one-time key signature
if (isVerified) { if (isVerified) {
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) sessionId = deviceInfo.identityKey()?.let { identityKey ->
olmDevice.createOutboundSession(identityKey, oneTimeKey.value)
}
if (!sessionId.isNullOrEmpty()) { if (sessionId.isNullOrEmpty()) {
Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId +
" for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
} else {
// Possibly a bad key // Possibly a bad key
Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
} else {
Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId")
} }
} else { } else {
Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage")
":" + deviceId + " Error " + errorMessage)
} }
} }

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -44,6 +45,8 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption(private val userId: String, internal class MXMegolmDecryption(private val userId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
@ -74,7 +77,7 @@ internal class MXMegolmDecryption(private val userId: String,
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
Timber.v("## CRYPTO | decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) { if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
} }
@ -230,7 +233,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param event the key event. * @param event the key event.
*/ */
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
Timber.v("## CRYPTO | onRoomKeyEvent()") Timber.tag(loggerTag.value).v("onRoomKeyEvent()")
var exportFormat = false var exportFormat = false
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
@ -239,11 +242,11 @@ internal class MXMegolmDecryption(private val userId: String,
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
return return
} }
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
Timber.i("## CRYPTO | onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
?: return ?: return
@ -252,7 +255,7 @@ internal class MXMegolmDecryption(private val userId: String,
} }
if (senderKey == null) { if (senderKey == null) {
Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field")
return return
} }
@ -261,20 +264,20 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat = true exportFormat = true
senderKey = forwardedRoomKeyContent.senderKey senderKey = forwardedRoomKeyContent.senderKey
if (null == senderKey) { if (null == senderKey) {
Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
return return
} }
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field") Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field")
return return
} }
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
} else { } else {
Timber.i("## CRYPTO | onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
if (null == senderKey) { if (null == senderKey) {
Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)")
return return
} }
@ -282,7 +285,7 @@ internal class MXMegolmDecryption(private val userId: String,
keysClaimed = event.getKeysClaimed().toMutableMap() keysClaimed = event.getKeysClaimed().toMutableMap()
} }
Timber.i("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey, roomKeyContent.sessionKey,
roomKeyContent.roomId, roomKeyContent.roomId,
@ -314,7 +317,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param sessionId the session id * @param sessionId the session id
*/ */
override fun onNewSession(senderKey: String, sessionId: String) { override fun onNewSession(senderKey: String, sessionId: String) {
Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey") Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(null, senderKey, sessionId) newSessionListener?.onNewSession(null, senderKey, sessionId)
} }
@ -346,10 +349,10 @@ internal class MXMegolmDecryption(private val userId: String,
if (olmSessionResult?.sessionId == null) { if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there // no session with this device, probably because there
// were no one-time keys. // were no one-time keys.
Timber.e("no session with this device $deviceId, probably because there were no one-time keys.") Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.")
return@mapCatching return@mapCatching
} }
Timber.i("## CRYPTO | shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) }
@ -360,7 +363,7 @@ internal class MXMegolmDecryption(private val userId: String,
}, },
{ {
// TODO // TODO
Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body")
} }
) )
@ -368,12 +371,12 @@ internal class MXMegolmDecryption(private val userId: String,
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload) sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.i("## CRYPTO | shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try { try {
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## CRYPTO | shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId")
} }
} }
} }

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -36,6 +37,8 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.internal.crypto.model.forEach import org.matrix.android.sdk.internal.crypto.model.forEach
import org.matrix.android.sdk.internal.crypto.model.toDebugCount
import org.matrix.android.sdk.internal.crypto.model.toDebugString
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
@ -43,6 +46,8 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.convertToUTF8 import org.matrix.android.sdk.internal.util.convertToUTF8
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO)
@Deprecated("in favour of rust") @Deprecated("in favour of rust")
internal class MXMegolmEncryption( internal class MXMegolmEncryption(
// The id of the room we will be sending to. // The id of the room we will be sending to.
@ -52,8 +57,8 @@ internal class MXMegolmEncryption(
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val userId: String, private val myUserId: String,
private val deviceId: String, private val myDeviceId: String,
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
@ -81,9 +86,10 @@ internal class MXMegolmEncryption(
eventType: String, eventType: String,
userIds: List<String>): Content { userIds: List<String>): Content {
val ts = System.currentTimeMillis() val ts = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds) val devices = getDevicesInRoom(userIds)
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
val outboundSession = ensureOutboundSession(devices.allowedDevices) val outboundSession = ensureOutboundSession(devices.allowedDevices)
return encryptContent(outboundSession, eventType, eventContent) return encryptContent(outboundSession, eventType, eventContent)
@ -92,7 +98,7 @@ internal class MXMegolmEncryption(
// annoyingly we have to serialize again the saved outbound session to store message index :/ // annoyingly we have to serialize again the saved outbound session to store message index :/
// if not we would see duplicate message index errors // if not we would see duplicate message index errors
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${System.currentTimeMillis() - ts} millis")
} }
} }
@ -119,13 +125,13 @@ internal class MXMegolmEncryption(
override suspend fun preshareKey(userIds: List<String>) { override suspend fun preshareKey(userIds: List<String>) {
val ts = System.currentTimeMillis() val ts = System.currentTimeMillis()
Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...")
val devices = getDevicesInRoom(userIds) val devices = getDevicesInRoom(userIds)
val outboundSession = ensureOutboundSession(devices.allowedDevices) val outboundSession = ensureOutboundSession(devices.allowedDevices)
notifyWithheldForSession(devices.withHeldDevices, outboundSession) notifyWithheldForSession(devices.withHeldDevices, outboundSession)
Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${System.currentTimeMillis() - ts} millis")
} }
/** /**
@ -134,7 +140,7 @@ internal class MXMegolmEncryption(
* @return the session description * @return the session description
*/ */
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
Timber.v("## CRYPTO | prepareNewSessionInRoom() ") Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
val keysClaimedMap = HashMap<String, String>() val keysClaimedMap = HashMap<String, String>()
@ -154,13 +160,14 @@ internal class MXMegolmEncryption(
* @param devicesInRoom the devices list * @param devicesInRoom the devices list
*/ */
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo { private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
Timber.v("## CRYPTO | ensureOutboundSession start") Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId")
var session = outboundSession var session = outboundSession
if (session == null || if (session == null ||
// Need to make a brand new session? // Need to make a brand new session?
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
// Determine if we have shared with anyone we shouldn't have // Determine if we have shared with anyone we shouldn't have
session.sharedWithTooManyDevices(devicesInRoom)) { session.sharedWithTooManyDevices(devicesInRoom)) {
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
session = prepareNewSessionInRoom() session = prepareNewSessionInRoom()
outboundSession = session outboundSession = session
} }
@ -177,6 +184,8 @@ internal class MXMegolmEncryption(
} }
} }
} }
val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size }
Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})")
shareKey(safeSession, shareMap) shareKey(safeSession, shareMap)
return safeSession return safeSession
} }
@ -191,7 +200,7 @@ internal class MXMegolmEncryption(
devicesByUsers: Map<String, List<CryptoDeviceInfo>>) { devicesByUsers: Map<String, List<CryptoDeviceInfo>>) {
// nothing to send, the task is done // nothing to send, the task is done
if (devicesByUsers.isEmpty()) { if (devicesByUsers.isEmpty()) {
Timber.v("## CRYPTO | shareKey() : nothing more to do") Timber.tag(loggerTag.value).v("shareKey() : nothing more to do")
return return
} }
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
@ -204,7 +213,7 @@ internal class MXMegolmEncryption(
break break
} }
} }
Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
shareUserDevicesKey(session, subMap) shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers - subMap.keys val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices) shareKey(session, remainingDevices)
@ -233,11 +242,11 @@ internal class MXMegolmEncryption(
payload["content"] = submap payload["content"] = submap
var t0 = System.currentTimeMillis() var t0 = System.currentTimeMillis()
Timber.v("## CRYPTO | shareUserDevicesKey() : starts") Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
Timber.v( Timber.tag(loggerTag.value).v(
"""## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms"""
.trimMargin() .trimMargin()
) )
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
@ -255,10 +264,11 @@ internal class MXMegolmEncryption(
// MSC 2399 // MSC 2399
// send withheld m.no_olm: an olm session could not be established. // 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. // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld")
noOlmToNotify.add(UserDevice(userId, deviceID)) noOlmToNotify.add(UserDevice(userId, deviceID))
continue continue
} }
Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
haveTargets = true haveTargets = true
} }
@ -276,7 +286,7 @@ internal class MXMegolmEncryption(
gossipingEventBuffer.add( gossipingEventBuffer.add(
Event( Event(
type = EventType.ROOM_KEY, type = EventType.ROOM_KEY,
senderId = this.userId, senderId = myUserId,
content = submap.apply { content = submap.apply {
this["session_key"] = "" this["session_key"] = ""
// we add a fake key for trail // we add a fake key for trail
@ -290,17 +300,18 @@ internal class MXMegolmEncryption(
if (haveTargets) { if (haveTargets) {
t0 = System.currentTimeMillis() t0 = System.currentTimeMillis()
Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
try { try {
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
} catch (failure: Throwable) { } catch (failure: Throwable) {
// What to do here... // What to do here...
Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
} }
} else { } else {
Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
} }
if (noOlmToNotify.isNotEmpty()) { if (noOlmToNotify.isNotEmpty()) {
@ -318,7 +329,8 @@ internal class MXMegolmEncryption(
sessionId: String, sessionId: String,
senderKey: String?, senderKey: String?,
code: WithHeldCode) { code: WithHeldCode) {
Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") Timber.tag(loggerTag.value).d("notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" +
" ${targets.joinToString { "${it.userId}|${it.deviceId}" }}")
val withHeldContent = RoomKeyWithHeldContent( val withHeldContent = RoomKeyWithHeldContent(
roomId = roomId, roomId = roomId,
senderKey = senderKey, senderKey = senderKey,
@ -337,7 +349,7 @@ internal class MXMegolmEncryption(
try { try {
sendToDeviceTask.execute(params) sendToDeviceTask.execute(params)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
} }
} }
@ -364,7 +376,7 @@ internal class MXMegolmEncryption(
// Include our device ID so that recipients can send us a // Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key. // m.new_device message if they don't have our session key.
map["device_id"] = deviceId map["device_id"] = myDeviceId
session.useCount++ session.useCount++
return map return map
} }
@ -425,9 +437,9 @@ internal class MXMegolmEncryption(
userId: String, userId: String,
deviceId: String, deviceId: String,
senderKey: String): Boolean { senderKey: String): Boolean {
Timber.i("## Crypto process reshareKey for $sessionId to $userId:$deviceId") Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId")
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
.also { Timber.w("## Crypto reshareKey: Device not found") } .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
// Get the chain index of the key we previously sent this device // Get the chain index of the key we previously sent this device
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo) val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo)
@ -435,13 +447,13 @@ internal class MXMegolmEncryption(
// This session was never shared with this user // This session was never shared with this user
// Send a room key with held // Send a room key with held
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.w("## Crypto reshareKey: ERROR : Never shared megolm with this device") Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
return false return false
} }
// if found chain index should not be null // if found chain index should not be null
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
.also { .also {
Timber.w("## Crypto reshareKey: Null chain index") Timber.tag(loggerTag.value).w("reshareKey: Null chain index")
} }
val devicesByUser = mapOf(userId to listOf(deviceInfo)) val devicesByUser = mapOf(userId to listOf(deviceInfo))
@ -450,10 +462,10 @@ internal class MXMegolmEncryption(
olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys. olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys.
// ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it.
?: return false.also { ?: return false.also {
Timber.w("## Crypto reshareKey: no session with this device, probably because there were no one-time keys") Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
} }
Timber.i("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
@ -465,7 +477,7 @@ internal class MXMegolmEncryption(
}, },
{ {
// TODO // TODO
Timber.e(it, "[MXMegolmEncryption] reshareKey: failed to get session $sessionId|$senderKey|$roomId") Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId")
} }
) )
@ -473,14 +485,14 @@ internal class MXMegolmEncryption(
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload) sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
return try { return try {
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
Timber.i("## CRYPTO reshareKey() : successfully send <$sessionId> to $userId:$deviceId") Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId")
true true
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## CRYPTO reshareKey() : fail to send <$sessionId> to $userId:$deviceId") Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
false false
} }
} }

View File

@ -52,8 +52,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
cryptoStore = cryptoStore, cryptoStore = cryptoStore,
deviceListManager = deviceListManager, deviceListManager = deviceListManager,
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction, ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,
userId = userId, myUserId = userId,
deviceId = deviceId!!, myDeviceId = deviceId!!,
sendToDeviceTask = sendToDeviceTask, sendToDeviceTask = sendToDeviceTask,
messageEncrypter = messageEncrypter, messageEncrypter = messageEncrypter,
warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository,

View File

@ -133,3 +133,11 @@ inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit)
} }
} }
} }
internal fun <T> MXUsersDevicesMap<T>.toDebugString() =
map.entries.joinToString { "${it.key} [${it.value.keys.joinToString { it }}]" }
internal fun <T> MXUsersDevicesMap<T>.toDebugCount() =
map.entries.fold(0) { acc, new ->
acc + new.value.keys.size
}

View File

@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface SendEventTask : Task<SendEventTask.Params, String> { internal interface SendEventTask : Task<SendEventTask.Params, String> {
@ -60,7 +61,9 @@ internal class DefaultSendEventTask @Inject constructor(
) )
} }
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT)
return response.eventId return response.eventId.also {
Timber.d("Event: $it just sent in ${params.event.roomId}")
}
} catch (e: Throwable) { } catch (e: Throwable) {
// localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED) // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED)
throw e throw e

View File

@ -26,20 +26,20 @@ internal object DraftMapper {
fun map(entity: DraftEntity): UserDraft { fun map(entity: DraftEntity): UserDraft {
return when (entity.draftMode) { return when (entity.draftMode) {
DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) DraftEntity.MODE_REGULAR -> UserDraft.Regular(entity.content)
DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) DraftEntity.MODE_EDIT -> UserDraft.Edit(entity.linkedEventId, entity.content)
DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) DraftEntity.MODE_QUOTE -> UserDraft.Quote(entity.linkedEventId, entity.content)
DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) DraftEntity.MODE_REPLY -> UserDraft.Reply(entity.linkedEventId, entity.content)
else -> null else -> null
} ?: UserDraft.REGULAR("") } ?: UserDraft.Regular("")
} }
fun map(domain: UserDraft): DraftEntity { fun map(domain: UserDraft): DraftEntity {
return when (domain) { return when (domain) {
is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") is UserDraft.Regular -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) is UserDraft.Edit -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) is UserDraft.Quote -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) is UserDraft.Reply -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
} }
} }
} }

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Index import io.realm.annotations.Index
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
@ -56,10 +57,10 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object companion object
fun setDecryptionResult(result: MXEventDecryptionResult) { fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
assertIsManaged() assertIsManaged()
val decryptionResult = OlmDecryptionResult( val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = clearEvent ?: result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain

View File

@ -16,8 +16,8 @@
package org.matrix.android.sdk.internal.session.content package org.matrix.android.sdk.internal.session.content
import org.matrix.android.sdk.api.MatrixUrls
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
@ -82,8 +82,8 @@ internal class DefaultContentUrlResolver @Inject constructor(
private fun resolve(contentUrl: String, private fun resolve(contentUrl: String,
toThumbnail: Boolean, toThumbnail: Boolean,
params: String = ""): String? { params: String = ""): String {
var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME) var serverAndMediaId = contentUrl.removeMxcPrefix()
val apiPath = if (scannerService.isScannerEnabled()) { val apiPath = if (scannerService.isScannerEnabled()) {
NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE

View File

@ -279,6 +279,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.e(failure, "## Failed to update file cache") Timber.e(failure, "## Failed to update file cache")
} }
// Delete the temporary voice message file
if (params.attachment.type == ContentAttachmentData.Type.VOICE_MESSAGE) {
context.contentResolver.delete(params.attachment.queryUri, null, null)
}
val uploadThumbnailResult = dealWithThumbnail(params) val uploadThumbnailResult = dealWithThumbnail(params)
handleSuccess(params, handleSuccess(params,
@ -299,11 +304,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
filesToDelete.forEach { filesToDelete.forEach {
tryOrNull { it.delete() } tryOrNull { it.delete() }
} }
// Delete the temporary voice message file
if (params.attachment.type == ContentAttachmentData.Type.AUDIO && params.attachment.mimeType == MimeTypes.Ogg) {
context.contentResolver.delete(params.attachment.queryUri, null, null)
}
} }
} }

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.contentscanner.tasks package org.matrix.android.sdk.internal.session.contentscanner.tasks
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
import org.matrix.android.sdk.api.failure.toScanFailure import org.matrix.android.sdk.api.failure.toScanFailure
import org.matrix.android.sdk.api.session.contentscanner.ScanState import org.matrix.android.sdk.api.session.contentscanner.ScanState
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -38,13 +40,13 @@ internal class DefaultScanMediaTask @Inject constructor(
override suspend fun execute(params: ScanMediaTask.Params): ScanResponse { override suspend fun execute(params: ScanMediaTask.Params): ScanResponse {
// "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ" // "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ"
if (!params.mxcUrl.startsWith("mxc://")) { if (!params.mxcUrl.isMxcUrl()) {
throw IllegalAccessException("Invalid mxc url") throw IllegalAccessException("Invalid mxc url")
} }
val scannerUrl = contentScannerStore.getScannerUrl() val scannerUrl = contentScannerStore.getScannerUrl()
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl) contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
var serverAndMediaId = params.mxcUrl.removePrefix("mxc://") var serverAndMediaId = params.mxcUrl.removeMxcPrefix()
val fragmentOffset = serverAndMediaId.indexOf("#") val fragmentOffset = serverAndMediaId.indexOf("#")
if (fragmentOffset >= 0) { if (fragmentOffset >= 0) {
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)

View File

@ -29,7 +29,6 @@ internal class DefaultEventService @Inject constructor(
override suspend fun getEvent(roomId: String, eventId: String): Event { override suspend fun getEvent(roomId: String, eventId: String): Event {
val event = getEventTask.execute(GetEventTask.Params(roomId, eventId)) val event = getEventTask.execute(GetEventTask.Params(roomId, eventId))
event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
// Fast lane to the call event processors: try to make the incoming call ring faster // Fast lane to the call event processors: try to make the incoming call ring faster
if (callEventProcessor.shouldProcessFastLane(event.getClearType())) { if (callEventProcessor.shouldProcessFastLane(event.getClearType())) {
callEventProcessor.processFastLane(event) callEventProcessor.processFastLane(event)

View File

@ -207,7 +207,8 @@ internal class LocalEchoEventFactory @Inject constructor(
return when (attachment.type) { return when (attachment.type) {
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false)
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true)
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
} }
} }
@ -296,8 +297,7 @@ internal class LocalEchoEventFactory @Inject constructor(
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content)
} }
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean): Event {
val isVoiceMessage = attachment.waveform != null
val content = MessageAudioContent( val content = MessageAudioContent(
msgType = MessageType.MSGTYPE_AUDIO, msgType = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",

View File

@ -23,6 +23,7 @@ import io.realm.RealmConfiguration
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
@ -33,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
@ -43,6 +45,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId 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.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.Debouncer
@ -72,6 +75,7 @@ internal class DefaultTimeline(
private val eventDecryptor: TimelineEventDecryptor, private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider, private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val readReceiptHandler: ReadReceiptHandler private val readReceiptHandler: ReadReceiptHandler
) : Timeline, ) : Timeline,
TimelineInput.Listener, TimelineInput.Listener,
@ -577,6 +581,10 @@ internal class DefaultTimeline(
} else { } else {
nextDisplayIndex = offsetIndex + 1 nextDisplayIndex = offsetIndex + 1
} }
// Prerequisite to in order for the ThreadsAwarenessHandler to work properly
fetchRootThreadEventsIfNeeded(offsetResults)
offsetResults.forEach { eventEntity -> offsetResults.forEach { eventEntity ->
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
@ -601,6 +609,20 @@ internal class DefaultTimeline(
return offsetResults.size return offsetResults.size
} }
/**
* This function is responsible to fetch and store the root event of a thread event
* in order to be able to display the event to the user appropriately
*/
private fun fetchRootThreadEventsIfNeeded(offsetResults: RealmResults<TimelineEventEntity>) = runBlocking {
val eventEntityList = offsetResults
.mapNotNull {
it?.root
}.map {
EventMapper.map(it)
}
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
}
private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {
return timelineEventMapper.map( return timelineEventMapper.map(
timelineEventEntity = eventEntity, timelineEventEntity = eventEntity,

View File

@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase 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.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor( internal class DefaultTimelineService @AssistedInject constructor(
@ -52,6 +53,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val readReceiptHandler: ReadReceiptHandler private val readReceiptHandler: ReadReceiptHandler
) : TimelineService { ) : TimelineService {
@ -75,6 +77,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider, realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask, loadRoomMembersTask = loadRoomMembersTask,
threadsAwarenessHandler = threadsAwarenessHandler,
readReceiptHandler = readReceiptHandler readReceiptHandler = readReceiptHandler
) )
} }

View File

@ -59,6 +59,8 @@ internal class DefaultGetEventTask @Inject constructor(
} }
} }
event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
return event return event
} }
} }

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -34,7 +35,8 @@ import javax.inject.Inject
internal class TimelineEventDecryptor @Inject constructor( internal class TimelineEventDecryptor @Inject constructor(
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val cryptoService: CryptoService private val cryptoService: CryptoService,
private val threadsAwarenessHandler: ThreadsAwarenessHandler
) { ) {
private val newSessionListener = object : NewSessionListener { private val newSessionListener = object : NewSessionListener {
@ -106,10 +108,19 @@ internal class TimelineEventDecryptor @Inject constructor(
val result = cryptoService.decryptEvent(request.event, timelineId) val result = cryptoService.decryptEvent(request.event, timelineId)
Timber.v("Successfully decrypted event ${event.eventId}") Timber.v("Successfully decrypted event ${event.eventId}")
realm.executeTransaction { realm.executeTransaction {
val eventId = event.eventId ?: "" val eventId = event.eventId ?: return@executeTransaction
EventEntity.where(it, eventId = eventId) val eventEntity = EventEntity
.where(it, eventId = eventId)
.findFirst() .findFirst()
?.setDecryptionResult(result)
eventEntity?.apply {
val decryptedPayload = threadsAwarenessHandler.handleIfNeededDuringDecryption(
it,
roomId = event.roomId,
event,
result)
setDecryptionResult(result, decryptedPayload)
}
} }
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")

View File

@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler
import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
@ -63,6 +64,7 @@ internal class SyncResponseHandler @Inject constructor(
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask, private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService, private val pushRuleService: PushRuleService,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val presenceSyncHandler: PresenceSyncHandler private val presenceSyncHandler: PresenceSyncHandler
) { ) {
@ -97,6 +99,10 @@ internal class SyncResponseHandler @Inject constructor(
Timber.v("Finish handling toDevice in $it ms") Timber.v("Finish handling toDevice in $it ms")
} }
val aggregator = SyncResponsePostTreatmentAggregator() val aggregator = SyncResponsePostTreatmentAggregator()
// Prerequisite for thread events handling in RoomSyncHandler
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
// Start one big transaction // Start one big transaction
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
measureTimeMillis { measureTimeMillis {

View File

@ -76,6 +76,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String, @UserId private val userId: String,
private val timelineInput: TimelineInput) { private val timelineInput: TimelineInput) {
@ -362,10 +363,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
eventIds.add(event.eventId) eventIds.add(event.eventId)
if (event.isEncrypted() && insertType != EventInsertType.INITIAL_SYNC) { val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
if (event.isEncrypted() && !isInitialSync) {
decryptIfNeeded(event, roomId) decryptIfNeeded(event, roomId)
} }
threadsAwarenessHandler.handleIfNeeded(
realm = realm,
roomId = roomId,
event = event)
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) { if (event.stateKey != null) {

View File

@ -0,0 +1,263 @@
/*
* Copyright 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.handler.room
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
/**
* This handler is responsible for a smooth threads migration. It will map all incoming
* threads as replies. So a device without threads enabled/updated will be able to view
* threads response as replies to the original message
*/
internal class ThreadsAwarenessHandler @Inject constructor(
private val permalinkFactory: PermalinkFactory,
private val cryptoService: CryptoService,
@SessionDatabase private val monarchy: Monarchy,
private val getEventTask: GetEventTask
) {
/**
* Fetch root thread events if they are missing from the local storage
* @param syncResponse the sync response
*/
suspend fun fetchRootThreadEventsIfNeeded(syncResponse: SyncResponse) {
val handlingStrategy = syncResponse.rooms?.join?.let {
RoomSyncHandler.HandlingStrategy.JOINED(it)
}
if (handlingStrategy !is RoomSyncHandler.HandlingStrategy.JOINED) return
val eventList = handlingStrategy.data
.mapNotNull { (roomId, roomSync) ->
roomSync.timeline?.events?.map {
it.copy(roomId = roomId)
}
}.flatten()
fetchRootThreadEventsIfNeeded(eventList)
}
/**
* Fetch root thread events if they are missing from the local storage
* @param eventList a list with the events to examine
*/
suspend fun fetchRootThreadEventsIfNeeded(eventList: List<Event>) {
if (eventList.isNullOrEmpty()) return
val threadsToFetch = emptyMap<String, String>().toMutableMap()
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
eventList.asSequence()
.filter {
isThreadEvent(it) && it.roomId != null
}.mapNotNull { event ->
getRootThreadEventId(event)?.let {
Pair(it, event.roomId!!)
}
}.forEach { (rootThreadEventId, roomId) ->
EventEntity.where(realm, rootThreadEventId).findFirst() ?: run { threadsToFetch[rootThreadEventId] = roomId }
}
}
fetchThreadsEvents(threadsToFetch)
}
/**
* Fetch multiple unique events using the fetchEvent function
*/
private suspend fun fetchThreadsEvents(threadsToFetch: Map<String, String>) {
val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) ->
fetchEvent(eventId, roomId)?.let {
it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs)
}
}
if (eventEntityList.isNullOrEmpty()) return
// Transaction should be done on its own thread, like below
monarchy.awaitTransaction { realm ->
eventEntityList.forEach {
it.copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
}
}
}
/**
* This function will fetch the event from the homeserver, this is mandatory when the
* initial thread message is too old and is not saved in the device, so in order to
* construct the "reply to" format we need to know the event thread.
* @return the Event or null otherwise
*/
private suspend fun fetchEvent(eventId: String, roomId: String): Event? {
return runCatching {
getEventTask.execute(GetEventTask.Params(roomId = roomId, eventId = eventId))
}.fold(
onSuccess = {
it
},
onFailure = {
null
})
}
/**
* Handle events mainly coming from the RoomSyncHandler
*/
fun handleIfNeeded(realm: Realm,
roomId: String,
event: Event) {
val payload = transformThreadToReplyIfNeeded(
realm = realm,
roomId = roomId,
event = event,
decryptedResult = event.mxDecryptionResult?.payload) ?: return
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload)
}
/**
* Handle events while they are being decrypted
*/
fun handleIfNeededDuringDecryption(realm: Realm,
roomId: String?,
event: Event,
result: MXEventDecryptionResult): JsonDict? {
return transformThreadToReplyIfNeeded(
realm = realm,
roomId = roomId,
event = event,
decryptedResult = result.clearEvent)
}
/**
* If the event is a thread event then transform/enhance it to a visual Reply Event,
* If the event is not a thread event, null value will be returned
* If there is an error (ex. the root/origin thread event is not found), null willl be returend
*/
private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? {
roomId ?: return null
if (!isThreadEvent(event)) return null
val rootThreadEventId = getRootThreadEventId(event) ?: return null
val payload = decryptedResult?.toMutableMap() ?: return null
val body = getValueFromPayload(payload, "body") ?: return null
val msgType = getValueFromPayload(payload, "msgtype") ?: return null
val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
decryptIfNeeded(rootThreadEvent, roomId)
val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body")
val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false)
val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
permalink,
userLink,
rootThreadEventSenderId,
// Remove inner mx_reply tags if any
rootThreadEventBody,
body)
val messageTextContent = MessageTextContent(
msgType = msgType,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = body,
formattedBody = replyFormatted
).toContent()
payload["content"] = messageTextContent
return payload
}
/**
* Decrypt the event
*/
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
if (!event.isEncrypted() || event.mxDecryptionResult != null) return
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
/**
* Try to get the event form the local DB, if the event does not exist null
* will be returned
*/
private fun getEventFromDB(realm: Realm, eventId: String): Event? {
val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() ?: return null
return EventMapper.map(eventEntity)
}
/**
* Returns True if the event is a thread
* @param event
*/
private fun isThreadEvent(event: Event): Boolean =
event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
/**
* Returns the root thread eventId or null otherwise
* @param event
*/
private fun getRootThreadEventId(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
val content = payload?.get("content") as? JsonDict
return content?.get(key) as? String
}
}

View File

@ -25,8 +25,8 @@ cd jitsi-meet
# This is commit after version 2.2.2, which does not compile # This is commit after version 2.2.2, which does not compile
# git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03 # git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03
# Version android-sdk-3.1.0, commit 7a64bf006ea027b77564d8847570e1ac46ff0ec0 # Version android-sdk-3.10.0, commit 99e56e229dfa3c490096e37c3e5b76d2a3f23e32
git checkout android-sdk-3.1.0 git checkout android-sdk-3.10.0
echo echo
echo "##################################################" echo "##################################################"

View File

@ -15,7 +15,7 @@ kapt {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 3 ext.versionMinor = 3
ext.versionPatch = 8 ext.versionPatch = 9
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -373,7 +373,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.38'
// FlowBinding // FlowBinding
implementation libs.github.flowBinding implementation libs.github.flowBinding
@ -470,10 +470,10 @@ dependencies {
// WebRTC // WebRTC
// org.webrtc:google-webrtc is for development purposes only // org.webrtc:google-webrtc is for development purposes only
// implementation 'org.webrtc:google-webrtc:1.0.+' // implementation 'org.webrtc:google-webrtc:1.0.+'
implementation('com.facebook.react:react-native-webrtc:1.87.3-jitsi-6624067@aar') implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar')
// Jitsi // Jitsi
implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') { implementation('org.jitsi.react:jitsi-meet-sdk:3.10.0') {
exclude group: 'com.google.firebase' exclude group: 'com.google.firebase'
exclude group: 'com.google.android.gms' exclude group: 'com.google.android.gms'
exclude group: 'com.android.installreferrer' exclude group: 'com.android.installreferrer'

View File

@ -68,6 +68,7 @@
<!-- Timber --> <!-- Timber -->
<!-- This rule is failing on CI because it's marked as unknwown rule id :/--> <!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
<!-- <issue id="BinaryOperationInTimber" severity="error" />--> <!-- <issue id="BinaryOperationInTimber" severity="error" />-->
<issue id="LogNotTimber" severity="error" />
<!-- Wording --> <!-- Wording -->
<issue id="Typos" severity="error" /> <issue id="Typos" severity="error" />
@ -83,4 +84,7 @@
<!-- Bug in lint agp 4.1 incorrectly thinks kotlin forEach is using java 8 API's. --> <!-- Bug in lint agp 4.1 incorrectly thinks kotlin forEach is using java 8 API's. -->
<!-- FIXME this workaround should be removed in a near future --> <!-- FIXME this workaround should be removed in a near future -->
<issue id="NewApi" severity="warning" /> <issue id="NewApi" severity="warning" />
<!-- DI -->
<issue id="JvmStaticProvidesInObjectDetector" severity="error" />
</lint> </lint>

View File

@ -68,6 +68,18 @@ object EspressoHelper {
} }
} }
fun withRetry(attempts: Int = 3, action: () -> Unit) {
runCatching { action() }.onFailure {
val remainingAttempts = attempts - 1
if (remainingAttempts <= 0) {
throw it
} else {
Thread.sleep(500)
withRetry(remainingAttempts, action)
}
}
}
fun getString(@StringRes id: Int): String { fun getString(@StringRes id: Int): String {
return EspressoHelper.getCurrentActivity()!!.resources.getString(id) return EspressoHelper.getCurrentActivity()!!.resources.getString(id)
} }
@ -235,11 +247,16 @@ fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) {
Espresso.pressBack() Espresso.pressBack()
} }
inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> interactWithSheet(contentMatcher: Matcher<View>, noinline block: () -> Unit = {}) { inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> interactWithSheet(
contentMatcher: Matcher<View>,
@BottomSheetBehavior.State openState: Int = BottomSheetBehavior.STATE_EXPANDED,
@BottomSheetBehavior.State exitState: Int = BottomSheetBehavior.STATE_HIDDEN,
noinline block: () -> Unit = {}
) {
waitUntilViewVisible(contentMatcher) waitUntilViewVisible(contentMatcher)
val behaviour = (EspressoHelper.getBottomSheetDialog<T>()!!.dialog as BottomSheetDialog).behavior val behaviour = (EspressoHelper.getBottomSheetDialog<T>()!!.dialog as BottomSheetDialog).behavior
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_EXPANDED), block) withIdlingResource(BottomSheetResource(behaviour, openState), block)
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_HIDDEN)) {} withIdlingResource(BottomSheetResource(behaviour, exitState)) {}
} }
class BottomSheetResource( class BottomSheetResource(

View File

@ -18,17 +18,25 @@ package im.vector.app.espresso.tools
import android.app.Activity import android.app.Activity
import android.view.View import android.view.View
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.activityIdlingResource import im.vector.app.activityIdlingResource
import im.vector.app.waitForView import im.vector.app.waitForView
import im.vector.app.withIdlingResource import im.vector.app.withIdlingResource
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers.not
inline fun <reified T : Activity> waitUntilActivityVisible(noinline block: (() -> Unit) = {}) { inline fun <reified T : Activity> waitUntilActivityVisible(noinline block: (() -> Unit) = {}) {
withIdlingResource(activityIdlingResource(T::class.java), block) withIdlingResource(activityIdlingResource(T::class.java), block)
} }
fun waitUntilViewVisible(viewMatcher: Matcher<View>) { fun waitUntilViewVisible(viewMatcher: Matcher<View>) {
Espresso.onView(ViewMatchers.isRoot()).perform(waitForView(viewMatcher)) onView(ViewMatchers.isRoot()).perform(waitForView(viewMatcher))
}
fun waitUntilDialogVisible(viewMatcher: Matcher<View>) {
onView(viewMatcher).inRoot(isDialog()).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
waitUntilViewVisible(viewMatcher)
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.ui package im.vector.app.ui
import androidx.test.espresso.IdlingPolicies
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
@ -30,6 +31,7 @@ import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit
/** /**
* This test aim to open every possible screen of the application * This test aim to open every possible screen of the application
@ -51,6 +53,8 @@ class UiAllScreensSanityTest {
// 2021-04-08 Testing 429 change // 2021-04-08 Testing 429 change
@Test @Test
fun allScreensTest() { fun allScreensTest() {
IdlingPolicies.setMasterPolicyTimeout(120, TimeUnit.SECONDS)
// Create an account // Create an account
val userId = "UiTest_" + UUID.randomUUID().toString() val userId = "UiTest_" + UUID.randomUUID().toString()
elementRobot.signUp(userId) elementRobot.signUp(userId)

View File

@ -28,6 +28,7 @@ import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDr
import im.vector.app.EspressoHelper import im.vector.app.EspressoHelper
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilDialogVisible
import im.vector.app.espresso.tools.waitUntilViewVisible import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
@ -104,17 +105,19 @@ class ElementRobot {
}.isSuccess }.isSuccess
if (expectSignOutWarning != isShowingSignOutWarning) { if (expectSignOutWarning != isShowingSignOutWarning) {
Timber.w("Unexpected sign out flow, expected warning to be: ${expectSignOutWarning.toWarningType()} but was ${isShowingSignOutWarning.toWarningType()}") val expected = expectSignOutWarning.toWarningType()
val actual = isShowingSignOutWarning.toWarningType()
Timber.w("Unexpected sign out flow, expected warning to be: $expected but was $actual")
} }
if (isShowingSignOutWarning) { if (isShowingSignOutWarning) {
// We have sent a message in a e2e room, accept to loose it // We have sent a message in a e2e room, accept to loose it
clickOn(R.id.exitAnywayButton) clickOn(R.id.exitAnywayButton)
// Dark pattern // Dark pattern
waitUntilViewVisible(withId(android.R.id.button2)) waitUntilDialogVisible(withId(android.R.id.button2))
clickDialogNegativeButton() clickDialogNegativeButton()
} else { } else {
waitUntilViewVisible(withId(android.R.id.button1)) waitUntilDialogVisible(withId(android.R.id.button1))
clickDialogPositiveButton() clickDialogPositiveButton()
} }

View File

@ -17,9 +17,13 @@
package im.vector.app.ui.robot package im.vector.app.ui.robot
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
import com.google.android.material.bottomsheet.BottomSheetBehavior
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.app.interactWithSheet
import java.lang.Thread.sleep import java.lang.Thread.sleep
class MessageMenuRobot( class MessageMenuRobot(
@ -36,7 +40,9 @@ class MessageMenuRobot(
fun editHistory() { fun editHistory() {
clickOn(R.string.message_view_edit_history) clickOn(R.string.message_view_edit_history)
interactWithSheet<ViewEditHistoryBottomSheet>(withText(R.string.message_edits), openState = BottomSheetBehavior.STATE_COLLAPSED) {
pressBack() pressBack()
}
autoClosed = true autoClosed = true
} }

View File

@ -26,6 +26,7 @@ import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assert
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.waitForView import im.vector.app.waitForView
class OnboardingRobot { class OnboardingRobot {
@ -42,6 +43,7 @@ class OnboardingRobot {
userId: String, userId: String,
password: String, password: String,
homeServerUrl: String) { homeServerUrl: String) {
waitUntilViewVisible(withId(R.id.loginSplashSubmit))
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit) assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit)
clickOn(R.id.loginSplashSubmit) clickOn(R.id.loginSplashSubmit)
assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) assertDisplayed(R.id.loginServerTitle, R.string.login_server_title)

View File

@ -30,12 +30,15 @@ import com.adevinta.android.barista.interaction.BaristaClickInteractions.longCli
import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu
import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu
import com.google.android.material.bottomsheet.BottomSheetBehavior
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilViewVisible import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.interactWithSheet import im.vector.app.interactWithSheet
import im.vector.app.waitForView import im.vector.app.waitForView
import im.vector.app.withRetry
import java.lang.Thread.sleep import java.lang.Thread.sleep
class RoomDetailRobot { class RoomDetailRobot {
@ -68,10 +71,14 @@ class RoomDetailRobot {
openMessageMenu(message) { openMessageMenu(message) {
addQuickReaction(quickReaction) addQuickReaction(quickReaction)
} }
println("Open reactions bottom sheet")
// Open reactions // Open reactions
longClickOn(quickReaction) longClickReaction(quickReaction)
// wait for bottom sheet // wait for bottom sheet
interactWithSheet<ViewReactionsBottomSheet>(withText(R.string.reactions), openState = BottomSheetBehavior.STATE_COLLAPSED) {
pressBack() pressBack()
}
println("Room Detail Robot: Open reaction from emoji picker")
// Test add reaction // Test add reaction
openMessageMenu(message) { openMessageMenu(message) {
addReactionFromEmojiPicker() addReactionFromEmojiPicker()
@ -81,16 +88,24 @@ class RoomDetailRobot {
edit() edit()
} }
// TODO Cancel action // TODO Cancel action
writeTo(R.id.composerEditText, "Hello universe!") val edit = "Hello universe - long message to avoid espresso tapping edited!"
writeTo(R.id.composerEditText, edit)
// Wait a bit for the keyboard layout to update // Wait a bit for the keyboard layout to update
waitUntilViewVisible(withId(R.id.sendButton)) waitUntilViewVisible(withId(R.id.sendButton))
clickOn(R.id.sendButton) clickOn(R.id.sendButton)
// Wait for the UI to update // Wait for the UI to update
waitUntilViewVisible(withText("Hello universe! (edited)")) waitUntilViewVisible(withText("$edit (edited)"))
// Open edit history // Open edit history
openMessageMenu("Hello universe! (edited)") { openMessageMenu("$edit (edited)") {
editHistory() editHistory()
} }
waitUntilViewVisible(withId(R.id.composerEditText))
}
private fun longClickReaction(quickReaction: String) {
withRetry {
longClickOn(quickReaction)
}
} }
fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) { fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) {
@ -111,7 +126,7 @@ class RoomDetailRobot {
} }
fun openSettings(block: RoomSettingsRobot.() -> Unit) { fun openSettings(block: RoomSettingsRobot.() -> Unit) {
clickOn(R.id.roomToolbarTitleView) clickMenu(R.id.timeline_setting)
waitForView(withId(R.id.roomProfileAvatarView)) waitForView(withId(R.id.roomProfileAvatarView))
sleep(1000) sleep(1000)
block(RoomSettingsRobot()) block(RoomSettingsRobot())

View File

@ -26,6 +26,7 @@ import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickD
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilDialogVisible
import im.vector.app.espresso.tools.waitUntilViewVisible import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity
@ -78,9 +79,9 @@ class RoomSettingsRobot {
// Room permissions // Room permissions
clickListItem(R.id.matrixProfileRecyclerView, 17) clickListItem(R.id.matrixProfileRecyclerView, 17)
waitUntilViewVisible(withText(R.string.room_permissions_title)) waitUntilViewVisible(withText(R.string.room_permissions_change_room_avatar))
clickOn(R.string.room_permissions_change_room_avatar) clickOn(R.string.room_permissions_change_room_avatar)
waitUntilViewVisible(withId(android.R.id.button2)) waitUntilDialogVisible(withId(android.R.id.button2))
clickDialogNegativeButton() clickDialogNegativeButton()
waitUntilViewVisible(withText(R.string.room_permissions_title)) waitUntilViewVisible(withText(R.string.room_permissions_title))
// Toggle // Toggle
@ -95,7 +96,7 @@ class RoomSettingsRobot {
private fun leaveRoom(block: DialogRobot.() -> Unit) { private fun leaveRoom(block: DialogRobot.() -> Unit) {
clickListItem(R.id.matrixProfileRecyclerView, 13) clickListItem(R.id.matrixProfileRecyclerView, 13)
waitUntilViewVisible(withId(android.R.id.button2)) waitUntilDialogVisible(withId(android.R.id.button2))
val dialogRobot = DialogRobot() val dialogRobot = DialogRobot()
block(dialogRobot) block(dialogRobot)
if (dialogRobot.returnedToPreviousScreen) { if (dialogRobot.returnedToPreviousScreen) {
@ -135,7 +136,7 @@ class RoomSettingsRobot {
// Role // Role
clickListItem(R.id.matrixProfileRecyclerView, 3) clickListItem(R.id.matrixProfileRecyclerView, 3)
waitUntilViewVisible(withId(android.R.id.button2)) waitUntilDialogVisible(withId(android.R.id.button2))
clickDialogNegativeButton() clickDialogNegativeButton()
waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView)) waitUntilViewVisible(withId(R.id.matrixProfileRecyclerView))
pressBack() pressBack()

View File

@ -5,6 +5,7 @@
<application> <application>
<activity android:name=".features.debug.TestLinkifyActivity" /> <activity android:name=".features.debug.TestLinkifyActivity" />
<activity android:name=".features.debug.DebugPermissionActivity" /> <activity android:name=".features.debug.DebugPermissionActivity" />
<activity android:name=".features.debug.settings.DebugPrivateSettingsActivity" />
<activity android:name=".features.debug.sas.DebugSasEmojiActivity" /> <activity android:name=".features.debug.sas.DebugSasEmojiActivity" />
</application> </application>

View File

@ -35,6 +35,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding import im.vector.app.databinding.ActivityDebugMenuBinding
import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.app.features.qrcode.QrCodeScannerActivity
import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkDefaultActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkDefaultActivity
import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkTestActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkTestActivity
@ -75,6 +76,7 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
} }
private fun setupViews() { private fun setupViews() {
views.debugPrivateSetting.setOnClickListener { openPrivateSettings() }
views.debugTestTextViewLink.setOnClickListener { testTextViewLink() } views.debugTestTextViewLink.setOnClickListener { testTextViewLink() }
views.debugOpenButtonStylesLight.setOnClickListener { views.debugOpenButtonStylesLight.setOnClickListener {
startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java)) startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java))
@ -115,6 +117,10 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
} }
} }
private fun openPrivateSettings() {
startActivity(Intent(this, DebugPrivateSettingsActivity::class.java))
}
private fun renderQrCode(text: String) { private fun renderQrCode(text: String) {
views.debugQrCode.setData(text) views.debugQrCode.setData(text)
} }

View File

@ -0,0 +1,36 @@
/*
* 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.debug.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.MavericksViewModelComponent
import im.vector.app.core.di.MavericksViewModelKey
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel
@InstallIn(MavericksViewModelComponent::class)
@Module
interface MavericksViewModelDebugModule {
@Binds
@IntoMap
@MavericksViewModelKey(DebugPrivateSettingsViewModel::class)
fun debugPrivateSettingsViewModelFactory(factory: DebugPrivateSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View File

@ -0,0 +1,38 @@
/*
* 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.debug.settings
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
@AndroidEntryPoint
class DebugPrivateSettingsActivity : VectorBaseActivity<ActivitySimpleBinding>() {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(
R.id.simpleFragmentContainer,
DebugPrivateSettingsFragment::class.java
)
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.debug.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDebugPrivateSettingsBinding
class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSettingsBinding>() {
private val viewModel: DebugPrivateSettingsViewModel by fragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDebugPrivateSettingsBinding {
return FragmentDebugPrivateSettingsBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setViewListeners()
}
private fun setViewListeners() {
views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
}
}
override fun invalidate() = withState(viewModel) {
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.debug.settings
import im.vector.app.core.platform.VectorViewModelAction
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
}

View File

@ -0,0 +1,65 @@
/*
* 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.debug.settings
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.settings.VectorDataStore
import kotlinx.coroutines.launch
class DebugPrivateSettingsViewModel @AssistedInject constructor(
@Assisted initialState: DebugPrivateSettingsViewState,
private val vectorDataStore: VectorDataStore
) : VectorViewModel<DebugPrivateSettingsViewState, DebugPrivateSettingsViewActions, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<DebugPrivateSettingsViewModel, DebugPrivateSettingsViewState> {
override fun create(initialState: DebugPrivateSettingsViewState): DebugPrivateSettingsViewModel
}
companion object : MavericksViewModelFactory<DebugPrivateSettingsViewModel, DebugPrivateSettingsViewState> by hiltMavericksViewModelFactory()
init {
observeVectorDataStore()
}
private fun observeVectorDataStore() {
vectorDataStore.forceDialPadDisplayFlow.setOnEach {
copy(
dialPadVisible = it
)
}
}
override fun handle(action: DebugPrivateSettingsViewActions) {
when (action) {
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
}
}
private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) {
viewModelScope.launch {
vectorDataStore.setForceDialPadDisplay(action.force)
}
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.debug.settings
import com.airbnb.mvrx.MavericksState
data class DebugPrivateSettingsViewState(
val dialPadVisible: Boolean = false
) : MavericksState

View File

@ -20,6 +20,12 @@
android:padding="@dimen/layout_horizontal_margin" android:padding="@dimen/layout_horizontal_margin"
android:showDividers="middle"> android:showDividers="middle">
<Button
android:id="@+id/debug_private_setting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Private settings" />
<Button <Button
android:id="@+id/debug_test_text_view_link" android:id="@+id/debug_test_text_view_link"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.debug.settings.DebugPrivateSettingsActivity"
tools:ignore="HardcodedText">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/linear_divider"
android:orientation="vertical"
android:padding="@dimen/layout_horizontal_margin"
android:showDividers="middle">
<CheckBox
android:id="@+id/forceDialPadTabDisplay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Force DialPad tab display" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -25,16 +25,12 @@ import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.fdroid.service.FDroidGuardServiceStarter import im.vector.app.fdroid.service.FDroidGuardServiceStarter
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class FlavorModule { @Module
object FlavorModule {
companion object {
@Provides @Provides
@JvmStatic
fun provideGuardServiceStarter(preferences: VectorPreferences, appContext: Context): GuardServiceStarter { fun provideGuardServiceStarter(preferences: VectorPreferences, appContext: Context): GuardServiceStarter {
return FDroidGuardServiceStarter(preferences, appContext) return FDroidGuardServiceStarter(preferences, appContext)
} }
}
} }

View File

@ -24,14 +24,10 @@ import im.vector.app.core.services.GuardServiceStarter
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@Module @Module
abstract class FlavorModule { object FlavorModule {
companion object {
@Provides @Provides
@JvmStatic
fun provideGuardServiceStarter(): GuardServiceStarter { fun provideGuardServiceStarter(): GuardServiceStarter {
return object : GuardServiceStarter {} return object : GuardServiceStarter {}
} }
}
} }

View File

@ -43,7 +43,7 @@
tools:node="remove" /> tools:node="remove" />
<!-- Jitsi SDK is now API23+ --> <!-- Jitsi SDK is now API23+ -->
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg" /> <uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react" />
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore --> <!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application --> <!-- Tell that the Camera is not mandatory to install the application -->
@ -424,14 +424,6 @@
<!-- Temporary fix for Android 12. android:exported has to be explicitly set when targeting Android 12 <!-- Temporary fix for Android 12. android:exported has to be explicitly set when targeting Android 12
Do it for services coming from dependencies - BEGIN --> Do it for services coming from dependencies - BEGIN -->
<service
android:name="org.jitsi.meet.sdk.ConnectionService"
android:exported="false"
tools:node="merge" />
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:exported="false"
tools:node="merge" />
<service <service
android:name="androidx.sharetarget.ChooserTargetServiceCompat" android:name="androidx.sharetarget.ChooserTargetServiceCompat"
android:exported="false" android:exported="false"

View File

@ -175,8 +175,7 @@ class VectorApplication :
} }
override fun onPause(owner: LifecycleOwner) { override fun onPause(owner: LifecycleOwner) {
Timber.i("App entered background") // call persistInfo Timber.i("App entered background")
notificationDrawerManager.persistInfo()
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
} }
}) })

View File

@ -71,6 +71,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore
keyRequestHandler.stop() keyRequestHandler.stop()
incomingVerificationRequestHandler.stop() incomingVerificationRequestHandler.stop()
pushRuleTriggerListener.stop() pushRuleTriggerListener.stop()
guardServiceStarter.stop()
} }
fun hasActiveSession(): Boolean { fun hasActiveSession(): Boolean {

View File

@ -28,7 +28,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineAsyncHelp
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
object HomeModule { object HomeModule {
@Provides @Provides
@JvmStatic
@TimelineEventControllerHandler @TimelineEventControllerHandler
fun providesTimelineBackgroundHandler(): Handler { fun providesTimelineBackgroundHandler(): Handler {
return TimelineAsyncHelper.getBackgroundHandler() return TimelineAsyncHelper.getBackgroundHandler()

View File

@ -41,7 +41,8 @@ import im.vector.app.features.home.PromoteRestrictedViewModel
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.UnreadMessagesSharedViewModel
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.composer.TextComposerViewModel import im.vector.app.features.home.room.detail.RoomDetailViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.search.SearchViewModel import im.vector.app.features.home.room.detail.search.SearchViewModel
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@ -505,8 +506,13 @@ interface MavericksViewModelModule {
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(TextComposerViewModel::class) @MavericksViewModelKey(RoomDetailViewModel::class)
fun textComposerViewModelFactory(factory: TextComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(MessageComposerViewModel::class)
fun messageComposerViewModelFactory(factory: MessageComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap

View File

@ -30,11 +30,9 @@ import im.vector.app.core.glide.GlideApp
object ScreenModule { object ScreenModule {
@Provides @Provides
@JvmStatic
fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context) fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context)
@Provides @Provides
@JvmStatic
@ActivityScoped @ActivityScoped
fun providesSharedViewPool() = RecyclerView.RecycledViewPool() fun providesSharedViewPool() = RecyclerView.RecycledViewPool()
} }

View File

@ -29,6 +29,8 @@ import dagger.hilt.components.SingletonComponent
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
import im.vector.app.features.navigation.DefaultNavigator import im.vector.app.features.navigation.DefaultNavigator
@ -66,6 +68,9 @@ abstract class VectorBindModule {
@Binds @Binds
abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
@Binds
abstract fun bindDefaultClock(clock: DefaultClock): Clock
} }
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -73,69 +78,58 @@ abstract class VectorBindModule {
object VectorStaticModule { object VectorStaticModule {
@Provides @Provides
@JvmStatic
fun providesContext(application: Application): Context { fun providesContext(application: Application): Context {
return application.applicationContext return application.applicationContext
} }
@Provides @Provides
@JvmStatic
fun providesResources(context: Context): Resources { fun providesResources(context: Context): Resources {
return context.resources return context.resources
} }
@Provides @Provides
@JvmStatic
fun providesSharedPreferences(context: Context): SharedPreferences { fun providesSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) return context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
} }
@Provides @Provides
@JvmStatic
fun providesMatrix(context: Context): Matrix { fun providesMatrix(context: Context): Matrix {
return Matrix.getInstance(context) return Matrix.getInstance(context)
} }
@Provides @Provides
@JvmStatic
fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session {
// TODO: handle session injection better // TODO: handle session injection better
return activeSessionHolder.getActiveSession() return activeSessionHolder.getActiveSession()
} }
@Provides @Provides
@JvmStatic
fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter { fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter {
return matrix.legacySessionImporter() return matrix.legacySessionImporter()
} }
@Provides @Provides
@JvmStatic
fun providesAuthenticationService(matrix: Matrix): AuthenticationService { fun providesAuthenticationService(matrix: Matrix): AuthenticationService {
return matrix.authenticationService() return matrix.authenticationService()
} }
@Provides @Provides
@JvmStatic
fun providesRawService(matrix: Matrix): RawService { fun providesRawService(matrix: Matrix): RawService {
return matrix.rawService() return matrix.rawService()
} }
@Provides @Provides
@JvmStatic
fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService {
return matrix.homeServerHistoryService() return matrix.homeServerHistoryService()
} }
@Provides @Provides
@JvmStatic
@Singleton @Singleton
fun providesApplicationCoroutineScope(): CoroutineScope { fun providesApplicationCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.Main) return CoroutineScope(SupervisorJob() + Dispatchers.Main)
} }
@Provides @Provides
@JvmStatic
fun providesCoroutineDispatchers(): CoroutineDispatchers { fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
} }

View File

@ -19,6 +19,7 @@ package im.vector.app.core.extensions
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@ -112,3 +113,11 @@ fun Activity.restart() {
startActivity(intent) startActivity(intent)
finish() finish()
} }
fun Activity.keepScreenOn() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
fun Activity.endKeepScreenOn() {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

View File

@ -16,9 +16,7 @@
package im.vector.app.core.extensions package im.vector.app.core.extensions
import android.os.Bundle
import android.util.Patterns import android.util.Patterns
import androidx.fragment.app.Fragment
import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.matrix.android.sdk.api.extensions.ensurePrefix import org.matrix.android.sdk.api.extensions.ensurePrefix
@ -27,11 +25,6 @@ fun Boolean.toOnOff() = if (this) "ON" else "OFF"
inline fun <T> T.ooi(block: (T) -> Unit): T = also(block) inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
/**
* Apply argument to a Fragment
*/
fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
/** /**
* Check if a CharSequence is an email * Check if a CharSequence is an email
*/ */

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