mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 09:25:49 +03:00
Merge tag 'v1.3.9' into sc
Change-Id: I26834af19e0b02887f288b6441ced6d993cb8861 Conflicts: vector/src/fdroid/java/im/vector/app/di/FlavorModule.kt vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedDistrib.kt vector/src/main/java/im/vector/app/VectorApplication.kt vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt vector/src/main/res/drawable/ic_shield_custom.xml vector/src/main/res/drawable/ic_shield_trusted.xml
This commit is contained in:
commit
4b63a9f939
198 changed files with 4263 additions and 2084 deletions
7
.github/workflows/sanity_test.yml
vendored
7
.github/workflows/sanity_test.yml
vendored
|
@ -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: |
|
||||||
|
|
|
@ -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
|
||||||
|
|
33
CHANGES.md
33
CHANGES.md
|
@ -1,8 +1,41 @@
|
||||||
|
Changes in Element v1.3.9 (2021-12-01)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Voice messages: Persist drafts of voice messages when navigating between rooms ([#3922](https://github.com/vector-im/element-android/issues/3922))
|
||||||
|
- Make Element Android Thread aware ([#4246](https://github.com/vector-im/element-android/issues/4246))
|
||||||
|
- Iterate on the consent dialog of the identity server. ([#4577](https://github.com/vector-im/element-android/issues/4577))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- 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 ([#3449](https://github.com/vector-im/element-android/issues/3449))
|
||||||
|
- Fixing queued voice message failing to send or retry ([#3833](https://github.com/vector-im/element-android/issues/3833))
|
||||||
|
- Keeping device screen on whilst recording and playing back voice messages ([#4022](https://github.com/vector-im/element-android/issues/4022))
|
||||||
|
- Allow voice messages to continue recording during device rotation ([#4067](https://github.com/vector-im/element-android/issues/4067))
|
||||||
|
- Allowing users to hang up VOIP calls during the initialisation phase (avoids getting stuck in the call screen if something goes wrong) ([#4144](https://github.com/vector-im/element-android/issues/4144))
|
||||||
|
- Make the verification shields the same in Element Web and Element Android ([#4338](https://github.com/vector-im/element-android/issues/4338))
|
||||||
|
- Fix a display issue in the composer when the replied message is changed. ([#4343](https://github.com/vector-im/element-android/issues/4343))
|
||||||
|
- Dismissing the Fdroid variant Listening for notifications on sign out, fixes crash when tapping the notification when signed out ([#4488](https://github.com/vector-im/element-android/issues/4488))
|
||||||
|
- Fix a crash when displaying the bootstrap bottom sheet ([#4520](https://github.com/vector-im/element-android/issues/4520))
|
||||||
|
- Remove duplicated settings declaration ([#4539](https://github.com/vector-im/element-android/issues/4539))
|
||||||
|
- Fixes .ogg files failing to upload to rooms ([#4552](https://github.com/vector-im/element-android/issues/4552))
|
||||||
|
- Add robustness when getting data from cursors ([#4605](https://github.com/vector-im/element-android/issues/4605))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Upgrade Jitsi lib (and so webrtc) from Jitsi android-sdk-3.1.0 to android-sdk-3.10.0 ([#4504](https://github.com/vector-im/element-android/issues/4504))
|
||||||
|
- Improve crypto logs to help debug decryption failures ([#4507](https://github.com/vector-im/element-android/issues/4507))
|
||||||
|
- Voice recording mic button refactor with small animation tweaks in preparation for voice drafts ([#4515](https://github.com/vector-im/element-android/issues/4515))
|
||||||
|
- Remove requestModelBuild() from epoxy Controllers init{} block ([#4591](https://github.com/vector-im/element-android/issues/4591))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.3.8 (2021-11-17)
|
Changes in Element v1.3.8 (2021-11-17)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
Features ✨
|
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))
|
- 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))
|
- 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))
|
- Adds support for images inside message notifications ([#4402](https://github.com/vector-im/element-android/issues/4402))
|
||||||
|
|
14
README.md
14
README.md
|
@ -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!
|
||||||
|
|
|
@ -74,9 +74,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()
|
||||||
|
|
|
@ -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.3"
|
||||||
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"
|
||||||
|
@ -46,13 +46,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",
|
||||||
|
|
|
@ -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.
|
||||||
|
|
2
fastlane/metadata/android/cs-CZ/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Hlavní změny v této verzi: Opravy chyb týkající se především oznámení.
|
||||||
|
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/de-DE/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Hauptänderungen: Fehler bei Benachrichtigungen gefixt
|
||||||
|
Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/en-US/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Add support for voice message draft. Many bugfixes!
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/et/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/et/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Põhilised muutused selles versioonis: erinevad veaparandused, neist enamus on seotud teavitustega.
|
||||||
|
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/fa/changelogs/40103050.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103050.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاقهای پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.5
|
2
fastlane/metadata/android/fa/changelogs/40103060.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103060.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاقهای پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.6
|
2
fastlane/metadata/android/fa/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
تغییرات اصلی در این نگارش: رفع اشکالهایی عمدتاً مربوط به آگاهیها.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/fr-FR/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Principaux changements pour cette version : corrections de problèmes, principalement sur les notifications
|
||||||
|
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/hu-HU/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/hu-HU/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Fő változás ebben a verzióban: Értesítési hibajavítások
|
||||||
|
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/id/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Perubahan utama di versi ini: Perbaikan bug terutama untuk notifikasinya.
|
||||||
|
Changelog lengkap: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/it-IT/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/it-IT/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Modifiche principali in questa versione: correzioni riguardo le notifiche.
|
||||||
|
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/pl/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/pl/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Główne zmiany w tej wersji: Poprawki błędów dotyczące głównie powiadomień
|
||||||
|
Pełny changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
42
fastlane/metadata/android/pl/full_description.txt
Normal file
42
fastlane/metadata/android/pl/full_description.txt
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Element jest bezpiecznym komunikatorem oraz narzędziem do komunikacji w zespole które jest idealna do pracy zdalnej. Nasza aplikacja korzysta z szyfrowania end-to-end aby rozmowy wideo, udostępnianie plików oraz rozmowy głosowe były bezpieczne.
|
||||||
|
|
||||||
|
<b>Funkcje Element'a:</b>
|
||||||
|
- Zaawansowane narzędzia komunikacji online
|
||||||
|
- W pełni szyfrowane wiadomości które umożliwiają bezpieczniejszą komunikacje dla firm, a nawet dla pracowników zdalnych.
|
||||||
|
- Zdecentralizowany czat bazowany na otwartym protokole Matrix
|
||||||
|
- Bezpieczne udostępnianie plików wraz z szyfrowaniem danych podczas zarządzania projektami
|
||||||
|
- Rozmowy z Voice over IP wraz z udostępnianiem ekranu
|
||||||
|
- Prosta konfiguracja z ulubionymi narzędziami do kolaboracji, narzędziami do zarządzania projektami usługami VoIP oraz innymi aplikacjami do komunikacji w grupie
|
||||||
|
|
||||||
|
Element jest całkowicie inny od innych komunikatorów i aplikacji do kolaboracji. Pracuje na protokole Matrix, otwarto źródłowej sieci stworzonej dla bezpiecznych wiadomości i zdecentralizowanej komunikacji. Pozwala ona na własny hosting serwera dla maksymalnej własności i kontroli nad danymi oraz wiadomościami.
|
||||||
|
|
||||||
|
<b>Prywatność i szyfrowane wiadomości</b>
|
||||||
|
Element broni cie przez niechcianymi wiadomościami, kopaniem informacji oraz cenzurą. Zabezpiecza wszystkie twoje dane, wideo które pozostaje wiadome tylko dla rozmawiających przez szyfrowanie end-to-end i weryfikacją krzyżową urządzeń.
|
||||||
|
|
||||||
|
Element daje kontrole nad twoją prywatnością i umożliwia bezpieczną komunikacje z kimkolwiek w sieci Matrix, lub z innymi firmami przez narzędzia do komunikacji integrując aplikacje takie jak Slack.
|
||||||
|
|
||||||
|
<b>Element może być hostowany samemu</b>
|
||||||
|
Pozwala to na kontrolę nad twoimi wrażliwymi danymi oraz rozmowami, Element może być hostowany samemu lub pozwala wybrać dowolnego hosta bazowanego na Matrix'ie - otwarto-źródłowym standardzie, dla zdecentralizowanej komunikacji. Element daje tobie prywatność, bezpieczeństwo oraz elastyczność w integracji.
|
||||||
|
|
||||||
|
<b>Posiadaj naprawdę swoje dane</b>
|
||||||
|
Ty decydujesz gdzie trzymasz swoje dane i wiadomości. Bez ryzyka wycieku lub dostępu firm trzecich.
|
||||||
|
|
||||||
|
Element daje ci kontrolę na wiele sposobów:
|
||||||
|
1. Utwórz darmowe konto na publicznym serwerze matrix.org hostowanym przez twórców Matrix'a lub wybierz którykolwiek z tysięcy serwerów hostowanych przez wolontariuszy
|
||||||
|
2. Hostuj samemu swoje konto przez własny serwer na twojej infrastrukturze
|
||||||
|
3. Zarejestruj się na specjalnym serwerze poprzez subskrybowanie hostingu na platformie Element Martix Services
|
||||||
|
|
||||||
|
<b>Otwarte wiadomości i kolaboracja</b>
|
||||||
|
Możesz rozmawiać z kimkolwiek w sieci Matrix, nie ważne czy korzystają z Element'a, czy z innej aplikacji wspierającej protokół Matrix, a nawet z osobami korzystającymi z innych komunikatorów.
|
||||||
|
|
||||||
|
<b>Niesamowicie bezpieczny</b>
|
||||||
|
Prawdziwe szyfrowanie end-to-end (tylko osoby w konwersacji mogą odszyfrować wiadomości), a także krzyżowa weryfikacja urządzeń.
|
||||||
|
|
||||||
|
<b>Pełna komunikacja i integracja</b>
|
||||||
|
Wiadomości, rozmowy głosowe i wideo, udostępnianie plików, ekranu, a nawet integracja z botami i widżetami. Twórz pokoje, społeczności, pozostań w kontakcie i załatwiaj to co chcesz.
|
||||||
|
|
||||||
|
<b>Kontynuuj gdzie skończyłeś</b>
|
||||||
|
Pozostań zawsze w kontakcie poprzez pełną synchornizację między urządzeniami oraz w sieci na https://app.element.io
|
||||||
|
|
||||||
|
<b>Otwarto źródłowy</b>
|
||||||
|
Element Android jest otwarto-źródłowym projektem, hostowanym na platformie GitHub. Prosimy o zgłaszanie wszelkich błędów i/lub wsparcie w tworzeniu naszego projektu na https://github.com/vector-im/element-android
|
1
fastlane/metadata/android/pl/short_description.txt
Normal file
1
fastlane/metadata/android/pl/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Grupowy komunikator - szyfrowane wiadomosci, grupowe czaty oraz rozmowy wideo
|
1
fastlane/metadata/android/pl/title.txt
Normal file
1
fastlane/metadata/android/pl/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Element - Bezpieczny Komunikator
|
2
fastlane/metadata/android/pt-BR/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Principais mudanças nesta versão: Consertos de bugs principalmente quanto às notificações.
|
||||||
|
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/sv-SE/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Huvudsakliga ändringar i den här versionen: Buggfixar som huvudsakligen rör aviseringar.
|
||||||
|
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/uk/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Основні зміни в цій версії: виправлення помилок в основному у повідомленнях.
|
||||||
|
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/zh-CN/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/zh-CN/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
此版本的主要变化:主要关于通知的错误修复。
|
||||||
|
完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/zh-TW/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40103070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
此版本中的主要變動:主要關於通知的臭蟲修復。
|
||||||
|
完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
113
library/ui-styles/src/debug/res/layout/debug_social_login.xml
Normal file
113
library/ui-styles/src/debug/res/layout/debug_social_login.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -165,7 +165,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>
|
||||||
|
|
|
@ -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()}\""
|
||||||
|
@ -115,7 +115,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'
|
||||||
|
@ -158,10 +158,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -22,6 +22,7 @@ import androidx.exifinterface.media.ExifInterface
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -44,8 +45,19 @@ data class ContentAttachmentData(
|
||||||
FILE,
|
FILE,
|
||||||
IMAGE,
|
IMAGE,
|
||||||
AUDIO,
|
AUDIO,
|
||||||
VIDEO
|
VIDEO,
|
||||||
|
VOICE_MESSAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
||||||
|
|
||||||
|
fun toJsonString(): String {
|
||||||
|
return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).toJson(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJsonString(json: String): ContentAttachmentData? {
|
||||||
|
return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).fromJson(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,15 +23,16 @@ 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 content: String) : UserDraft
|
||||||
data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text)
|
data class Quote(val linkedEventId: String, val content: String) : UserDraft
|
||||||
data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text)
|
data class Edit(val linkedEventId: String, val content: String) : UserDraft
|
||||||
data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text)
|
data class Reply(val linkedEventId: String, val content: String) : UserDraft
|
||||||
|
data class Voice(val content: String) : UserDraft
|
||||||
|
|
||||||
fun isValid(): Boolean {
|
fun isValid(): Boolean {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is REGULAR -> text.isNotBlank()
|
is Regular -> content.isNotBlank()
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,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.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
@ -110,6 +111,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(
|
||||||
// Olm Manager
|
// Olm Manager
|
||||||
|
@ -346,7 +350,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "## CRYPTO onSyncWillProcess ")
|
Timber.tag(loggerTag.value).e(failure, "onSyncWillProcess ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -379,7 +383,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
{
|
{
|
||||||
isStarting.set(false)
|
isStarting.set(false)
|
||||||
isStarted.set(false)
|
isStarted.set(false)
|
||||||
Timber.e(it, "Start failed")
|
Timber.tag(loggerTag.value).e(it, "Start failed")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -551,14 +555,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
|
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
|
||||||
|
|
||||||
if (!encryptingClass) {
|
if (!encryptingClass) {
|
||||||
Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
|
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,7 +581,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)
|
||||||
|
|
||||||
|
@ -655,17 +659,17 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val safeAlgorithm = alg
|
val safeAlgorithm = alg
|
||||||
if (safeAlgorithm != null) {
|
if (safeAlgorithm != null) {
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
Timber.v("## CRYPTO | encryptEventContent() starts")
|
Timber.tag(loggerTag.value).v("encryptEventContent() starts")
|
||||||
runCatching {
|
runCatching {
|
||||||
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||||
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 algorithm = getEncryptionAlgorithm(roomId)
|
val algorithm = getEncryptionAlgorithm(roomId)
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
||||||
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
algorithm ?: 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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,7 +681,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
if (roomEncryptor is IMXGroupEncryption) {
|
if (roomEncryptor is IMXGroupEncryption) {
|
||||||
roomEncryptor.discardSessionKey()
|
roomEncryptor.discardSessionKey()
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## CRYPTO | discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption")
|
Timber.tag(loggerTag.value).e("discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -768,14 +772,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun onRoomKeyEvent(event: Event) {
|
private fun onRoomKeyEvent(event: Event) {
|
||||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||||
Timber.i("## CRYPTO | onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||||
Timber.e("## CRYPTO | onRoomKeyEvent() : missing fields")
|
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
||||||
if (alg == null) {
|
if (alg == null) {
|
||||||
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alg.onRoomKeyEvent(event, keysBackupService)
|
alg.onRoomKeyEvent(event, keysBackupService)
|
||||||
|
@ -783,29 +787,29 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
private fun onKeyWithHeldReceived(event: Event) {
|
private fun onKeyWithHeldReceived(event: Event) {
|
||||||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||||
Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
|
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
}
|
}
|
||||||
Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||||
if (alg is IMXWithHeldExtension) {
|
if (alg is IMXWithHeldExtension) {
|
||||||
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## CRYPTO | onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSecretSendReceived(event: Event) {
|
private fun onSecretSendReceived(event: Event) {
|
||||||
Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||||
if (!event.isEncrypted()) {
|
if (!event.isEncrypted()) {
|
||||||
// secret send messages must be encrypted
|
// secret send messages must be encrypted
|
||||||
Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event")
|
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Was that sent by us?
|
// Was that sent by us?
|
||||||
if (event.senderId != userId) {
|
if (event.senderId != userId) {
|
||||||
Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -815,13 +819,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
||||||
|
|
||||||
if (existingRequest == null) {
|
if (existingRequest == null) {
|
||||||
Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
||||||
// TODO Ask to application layer?
|
// TODO Ask to application layer?
|
||||||
Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK")
|
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -858,7 +862,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
|
||||||
}
|
}
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
@ -912,7 +916,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private suspend fun uploadDeviceKeys() {
|
private suspend fun uploadDeviceKeys() {
|
||||||
if (cryptoStore.areDeviceKeysUploaded()) {
|
if (cryptoStore.areDeviceKeysUploaded()) {
|
||||||
Timber.d("Keys already uploaded, nothing to do")
|
Timber.tag(loggerTag.value).d("Keys already uploaded, nothing to do")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Prepare the device keys data to send
|
// Prepare the device keys data to send
|
||||||
|
@ -971,13 +975,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
password: String,
|
password: String,
|
||||||
progressListener: ProgressListener?): ImportRoomKeysResult {
|
progressListener: ProgressListener?): ImportRoomKeysResult {
|
||||||
return withContext(coroutineDispatchers.crypto) {
|
return withContext(coroutineDispatchers.crypto) {
|
||||||
Timber.v("## CRYPTO | importRoomKeys starts")
|
Timber.tag(loggerTag.value).v("importRoomKeys starts")
|
||||||
|
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
||||||
val t1 = System.currentTimeMillis()
|
val t1 = System.currentTimeMillis()
|
||||||
|
|
||||||
Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
||||||
|
|
||||||
val importedSessions = MoshiProvider.providesMoshi()
|
val importedSessions = MoshiProvider.providesMoshi()
|
||||||
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
||||||
|
@ -985,7 +989,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
val t2 = System.currentTimeMillis()
|
val t2 = System.currentTimeMillis()
|
||||||
|
|
||||||
Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
|
Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms")
|
||||||
|
|
||||||
if (importedSessions == null) {
|
if (importedSessions == null) {
|
||||||
throw Exception("Error")
|
throw Exception("Error")
|
||||||
|
@ -1122,7 +1126,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||||
Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content")
|
Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
val requestBody = RoomKeyRequestBody(
|
||||||
|
@ -1137,7 +1141,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
override fun requestRoomKeyForEvent(event: Event) {
|
override fun requestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||||
Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
@ -1148,7 +1152,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
roomDecryptorProvider
|
roomDecryptorProvider
|
||||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||||
?.requestKeysForEvent(event, false) ?: run {
|
?.requestKeysForEvent(event, false) ?: run {
|
||||||
Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1287,12 +1291,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
|
||||||
}
|
}
|
||||||
|
@ -1305,7 +1309,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
if (alg == null) {
|
if (alg == 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
|
||||||
}
|
}
|
||||||
|
@ -1315,7 +1319,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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,11 +84,11 @@ 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()}")
|
||||||
|
|
||||||
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
|
||||||
|
@ -83,7 +96,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
|
||||||
}
|
}
|
||||||
|
@ -92,12 +105,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,31 +124,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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
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.
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
|
@ -51,8 +56,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,
|
||||||
|
@ -80,9 +85,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)
|
||||||
|
@ -91,7 +97,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,13 +124,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,7 +139,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>()
|
||||||
|
@ -153,13 +159,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
|
||||||
}
|
}
|
||||||
|
@ -176,6 +183,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
|
||||||
}
|
}
|
||||||
|
@ -190,7 +199,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)
|
||||||
|
@ -203,7 +212,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)
|
||||||
|
@ -232,11 +241,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>()
|
||||||
|
@ -254,10 +263,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
|
||||||
}
|
}
|
||||||
|
@ -275,7 +285,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
|
||||||
|
@ -289,17 +299,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()) {
|
||||||
|
@ -317,7 +328,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,
|
||||||
|
@ -336,7 +348,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 ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +375,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
|
||||||
}
|
}
|
||||||
|
@ -424,9 +436,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)
|
||||||
|
@ -434,13 +446,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))
|
||||||
|
@ -449,10 +461,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)
|
||||||
|
|
||||||
|
@ -464,7 +476,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -472,14 +484,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -129,3 +129,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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -26,20 +26,22 @@ 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)
|
||||||
|
DraftEntity.MODE_VOICE -> UserDraft.Voice(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.content, 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.content, 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.content, 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.content, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
|
||||||
|
is UserDraft.Voice -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_VOICE, linkedEventId = "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import io.realm.RealmObject
|
||||||
internal open class DraftEntity(var content: String = "",
|
internal open class DraftEntity(var content: String = "",
|
||||||
var draftMode: String = MODE_REGULAR,
|
var draftMode: String = MODE_REGULAR,
|
||||||
var linkedEventId: String = ""
|
var linkedEventId: String = ""
|
||||||
|
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -29,5 +28,6 @@ internal open class DraftEntity(var content: String = "",
|
||||||
const val MODE_EDIT = "EDIT"
|
const val MODE_EDIT = "EDIT"
|
||||||
const val MODE_REPLY = "REPLY"
|
const val MODE_REPLY = "REPLY"
|
||||||
const val MODE_QUOTE = "QUOTE"
|
const val MODE_QUOTE = "QUOTE"
|
||||||
|
const val MODE_VOICE = "VOICE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -282,6 +282,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,
|
||||||
|
@ -302,11 +307,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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -208,7 +208,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,8 +302,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",
|
||||||
|
|
|
@ -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
|
||||||
|
@ -73,6 +76,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,
|
||||||
|
@ -595,6 +599,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)
|
||||||
|
@ -619,6 +627,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,
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,8 @@ internal class DefaultGetEventTask @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
|
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -41,6 +41,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
|
||||||
|
@ -65,6 +66,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 {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.sync.handler
|
package org.matrix.android.sdk.internal.session.sync.handler
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -32,6 +33,8 @@ import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
|
internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
|
||||||
private val verificationService: DefaultVerificationService) {
|
private val verificationService: DefaultVerificationService) {
|
||||||
|
|
||||||
|
@ -40,11 +43,11 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
|
||||||
toDevice.events?.forEachIndexed { index, event ->
|
toDevice.events?.forEachIndexed { index, event ->
|
||||||
progressReporter?.reportProgress(index * 100F / total)
|
progressReporter?.reportProgress(index * 100F / total)
|
||||||
// Decrypt event if necessary
|
// Decrypt event if necessary
|
||||||
Timber.i("## CRYPTO | To device event from ${event.senderId} of type:${event.type}")
|
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
|
||||||
decryptToDeviceEvent(event, null)
|
decryptToDeviceEvent(event, null)
|
||||||
if (event.getClearType() == EventType.MESSAGE &&
|
if (event.getClearType() == EventType.MESSAGE &&
|
||||||
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
||||||
Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||||
} else {
|
} else {
|
||||||
verificationService.onToDeviceEvent(event)
|
verificationService.onToDeviceEvent(event)
|
||||||
cryptoService.onToDeviceEvent(event)
|
cryptoService.onToDeviceEvent(event)
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -363,10 +364,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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
|
import androidx.core.database.getIntOrNull
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
||||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
|
|
||||||
|
@ -54,9 +56,9 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||||
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
|
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
|
||||||
|
|
||||||
val contactId = cursor.getInt(idColumn)
|
val contactId = cursor.getIntOrNull(idColumn) ?: return@use
|
||||||
var name = cursor.getString(nameColumn)
|
var name = cursor.getStringOrNull(nameColumn) ?: return@use
|
||||||
val photoUri = cursor.getString(photoUriColumn)
|
val photoUri = cursor.getStringOrNull(photoUriColumn)
|
||||||
val phoneNumberList = mutableListOf<String>()
|
val phoneNumberList = mutableListOf<String>()
|
||||||
val emailList = mutableListOf<String>()
|
val emailList = mutableListOf<String>()
|
||||||
|
|
||||||
|
@ -78,8 +80,8 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
|
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
|
||||||
|
|
||||||
while (innerCursor.moveToNext()) {
|
while (innerCursor.moveToNext()) {
|
||||||
val mimeType = innerCursor.getString(mimeTypeColumnIndex)
|
val mimeType = innerCursor.getStringOrNull(mimeTypeColumnIndex)
|
||||||
val contactData = innerCursor.getString(data1ColumnIndex)
|
val contactData = innerCursor.getStringOrNull(data1ColumnIndex) ?: continue
|
||||||
|
|
||||||
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||||
name = contactData
|
name = contactData
|
||||||
|
@ -121,7 +123,7 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
return if (cursor.moveToFirst()) {
|
return if (cursor.moveToFirst()) {
|
||||||
cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
|
cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
|
||||||
?.let { cursor.getInt(it) }
|
?.let { cursor.getIntOrNull(it) }
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ package im.vector.lib.multipicker
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.database.getLongOrNull
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
||||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
|
@ -53,8 +55,8 @@ class FilePicker : Picker<MultiPickerBaseType>() {
|
||||||
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
|
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
|
||||||
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
|
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getStringOrNull(nameColumn)
|
||||||
val size = cursor.getLong(sizeColumn)
|
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||||
|
|
||||||
MultiPickerFileType(
|
MultiPickerFileType(
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -20,6 +20,8 @@ import android.content.Context
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.database.getLongOrNull
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||||
import im.vector.lib.multipicker.entity.MultiPickerVideoType
|
import im.vector.lib.multipicker.entity.MultiPickerVideoType
|
||||||
|
@ -42,8 +44,8 @@ internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType?
|
||||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getStringOrNull(nameColumn)
|
||||||
val size = cursor.getLong(sizeColumn)
|
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||||
|
|
||||||
val bitmap = ImageUtils.getBitmap(context, this)
|
val bitmap = ImageUtils.getBitmap(context, this)
|
||||||
val orientation = ImageUtils.getOrientation(context, this)
|
val orientation = ImageUtils.getOrientation(context, this)
|
||||||
|
@ -80,8 +82,8 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
|
||||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getStringOrNull(nameColumn)
|
||||||
val size = cursor.getLong(sizeColumn)
|
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||||
var duration = 0L
|
var duration = 0L
|
||||||
var width = 0
|
var width = 0
|
||||||
var height = 0
|
var height = 0
|
||||||
|
@ -133,8 +135,8 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
||||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
|
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
|
||||||
|
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
val name = cursor.getString(nameColumn)
|
val name = cursor.getStringOrNull(nameColumn)
|
||||||
val size = cursor.getLong(sizeColumn)
|
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||||
var duration = 0L
|
var duration = 0L
|
||||||
|
|
||||||
context.contentResolver.openFileDescriptor(this, "r")?.use { pfd ->
|
context.contentResolver.openFileDescriptor(this, "r")?.use { pfd ->
|
||||||
|
|
|
@ -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 "##################################################"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
ext.scVersion = 47
|
ext.scVersion = 47
|
||||||
|
|
||||||
|
@ -386,7 +386,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
|
||||||
|
@ -485,10 +485,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'
|
||||||
|
|
|
@ -84,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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<*, *>
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -51,7 +51,7 @@
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<!-- 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 -->
|
||||||
|
@ -466,14 +466,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"
|
||||||
|
|
|
@ -179,8 +179,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()
|
|
||||||
StateHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
StateHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,8 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.database.getLongOrNull
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -61,8 +63,8 @@ class ContactsDataSource @Inject constructor(
|
||||||
val displayNameColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
val displayNameColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||||
val photoUriColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Data.PHOTO_URI)
|
val photoUriColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Data.PHOTO_URI)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idColumnIndex)
|
val id = cursor.getLongOrNull(idColumnIndex) ?: continue
|
||||||
val displayName = cursor.getString(displayNameColumnIndex)
|
val displayName = cursor.getStringOrNull(displayNameColumnIndex) ?: continue
|
||||||
|
|
||||||
val mappedContactBuilder = MappedContactBuilder(
|
val mappedContactBuilder = MappedContactBuilder(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -70,7 +72,7 @@ class ContactsDataSource @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
photoUriColumnIndex
|
photoUriColumnIndex
|
||||||
?.let { cursor.getString(it) }
|
?.let { cursor.getStringOrNull(it) }
|
||||||
?.let { Uri.parse(it) }
|
?.let { Uri.parse(it) }
|
||||||
?.let { mappedContactBuilder.photoURI = it }
|
?.let { mappedContactBuilder.photoURI = it }
|
||||||
|
|
||||||
|
@ -94,10 +96,10 @@ class ContactsDataSource @Inject constructor(
|
||||||
val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use
|
val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
val mappedContactBuilder = cursor.getLongOrNull(idColumnIndex)
|
||||||
.let { map[it] }
|
?.let { map[it] }
|
||||||
?: continue
|
?: continue
|
||||||
cursor.getString(phoneNumberColumnIndex)
|
cursor.getStringOrNull(phoneNumberColumnIndex)
|
||||||
?.let {
|
?.let {
|
||||||
mappedContactBuilder.msisdns.add(
|
mappedContactBuilder.msisdns.add(
|
||||||
MappedMsisdn(
|
MappedMsisdn(
|
||||||
|
@ -128,10 +130,10 @@ class ContactsDataSource @Inject constructor(
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
// This would allow you get several email addresses
|
// This would allow you get several email addresses
|
||||||
// if the email addresses were stored in an array
|
// if the email addresses were stored in an array
|
||||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
val mappedContactBuilder = cursor.getLongOrNull(idColumnIndex)
|
||||||
.let { map[it] }
|
?.let { map[it] }
|
||||||
?: continue
|
?: continue
|
||||||
cursor.getString(emailColumnIndex)
|
cursor.getStringOrNull(emailColumnIndex)
|
||||||
?.let {
|
?.let {
|
||||||
mappedContactBuilder.emails.add(
|
mappedContactBuilder.emails.add(
|
||||||
MappedEmail(
|
MappedEmail(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.intent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||||
|
|
||||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||||
|
@ -27,7 +28,7 @@ fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||||
?.use { cursor ->
|
?.use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
return cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME)
|
return cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME)
|
||||||
?.let { cursor.getString(it) }
|
?.let { cursor.getStringOrNull(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -60,6 +61,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.extensions.restart
|
import im.vector.app.core.extensions.restart
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.core.extensions.singletonEntryPoint
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
import im.vector.app.core.flow.throttleFirst
|
import im.vector.app.core.flow.throttleFirst
|
||||||
import im.vector.app.core.utils.toast
|
import im.vector.app.core.utils.toast
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
@ -385,9 +387,9 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||||
bugReporter.inMultiWindowMode = isInMultiWindowMode
|
bugReporter.inMultiWindowMode = isInMultiWindowMode
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun createFragment(fragmentClass: Class<out Fragment>, args: Bundle?): Fragment {
|
protected fun createFragment(fragmentClass: Class<out Fragment>, argsParcelable: Parcelable? = null): Fragment {
|
||||||
return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply {
|
return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply {
|
||||||
arguments = args
|
arguments = argsParcelable?.toMvRxBundle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,7 +556,8 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||||
|
|
||||||
open fun initUiAndData() = Unit
|
open fun initUiAndData() = Unit
|
||||||
|
|
||||||
override fun invalidate() = Unit
|
// Note: does not seem to be called
|
||||||
|
final override fun invalidate() = Unit
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
open fun getTitleRes() = -1
|
open fun getTitleRes() = -1
|
||||||
|
|
|
@ -28,13 +28,13 @@ import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.airbnb.mvrx.Mavericks
|
|
||||||
import com.airbnb.mvrx.MavericksView
|
import com.airbnb.mvrx.MavericksView
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import im.vector.app.core.di.ActivityEntryPoint
|
import im.vector.app.core.di.ActivityEntryPoint
|
||||||
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
import im.vector.app.core.flow.throttleFirst
|
import im.vector.app.core.flow.throttleFirst
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
@ -159,7 +159,7 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setArguments(args: Parcelable? = null) {
|
protected fun setArguments(args: Parcelable? = null) {
|
||||||
arguments = args?.let { Bundle().apply { putParcelable(Mavericks.KEY_ARG, it) } }
|
arguments = args.toMvRxBundle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
|
|
36
vector/src/main/java/im/vector/app/core/time/Clock.kt
Normal file
36
vector/src/main/java/im/vector/app/core/time/Clock.kt
Normal 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.core.time
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface Clock {
|
||||||
|
fun epochMillis(): Long
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultClock @Inject constructor() : Clock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a UTC epoch in milliseconds
|
||||||
|
*
|
||||||
|
* This value is not guaranteed to be correct with reality
|
||||||
|
* as a User can override the system time and date to any values.
|
||||||
|
*/
|
||||||
|
override fun epochMillis(): Long {
|
||||||
|
return System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,21 +36,30 @@ class ShieldImageView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) {
|
fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
|
||||||
isVisible = roomEncryptionTrustLevel != null
|
isVisible = roomEncryptionTrustLevel != null
|
||||||
|
|
||||||
when (roomEncryptionTrustLevel) {
|
when (roomEncryptionTrustLevel) {
|
||||||
RoomEncryptionTrustLevel.Default -> {
|
RoomEncryptionTrustLevel.Default -> {
|
||||||
contentDescription = context.getString(R.string.a11y_trust_level_default)
|
contentDescription = context.getString(R.string.a11y_trust_level_default)
|
||||||
setImageResource(R.drawable.ic_shield_black)
|
setImageResource(
|
||||||
|
if (borderLess) R.drawable.ic_shield_black_no_border
|
||||||
|
else R.drawable.ic_shield_black
|
||||||
|
)
|
||||||
}
|
}
|
||||||
RoomEncryptionTrustLevel.Warning -> {
|
RoomEncryptionTrustLevel.Warning -> {
|
||||||
contentDescription = context.getString(R.string.a11y_trust_level_warning)
|
contentDescription = context.getString(R.string.a11y_trust_level_warning)
|
||||||
setImageResource(R.drawable.ic_shield_warning)
|
setImageResource(
|
||||||
|
if (borderLess) R.drawable.ic_shield_warning_no_border
|
||||||
|
else R.drawable.ic_shield_warning
|
||||||
|
)
|
||||||
}
|
}
|
||||||
RoomEncryptionTrustLevel.Trusted -> {
|
RoomEncryptionTrustLevel.Trusted -> {
|
||||||
contentDescription = context.getString(R.string.a11y_trust_level_trusted)
|
contentDescription = context.getString(R.string.a11y_trust_level_trusted)
|
||||||
setImageResource(R.drawable.ic_shield_trusted)
|
setImageResource(
|
||||||
|
if (borderLess) R.drawable.ic_shield_trusted_no_border
|
||||||
|
else R.drawable.ic_shield_trusted
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,15 @@
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.TextView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.discovery.IdentityServerWithTerms
|
||||||
|
import me.gujun.android.span.link
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a web view above the current activity.
|
* Open a web view above the current activity.
|
||||||
|
@ -40,16 +45,36 @@ fun Context.displayInWebView(url: String) {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, policyLinkCallback: () -> Unit, consentCallBack: (() -> Unit)) {
|
fun Context.showIdentityServerConsentDialog(identityServerWithTerms: IdentityServerWithTerms?,
|
||||||
|
consentCallBack: (() -> Unit)) {
|
||||||
|
// Build the message
|
||||||
|
val content = span {
|
||||||
|
+getString(R.string.identity_server_consent_dialog_content_3)
|
||||||
|
+"\n\n"
|
||||||
|
if (identityServerWithTerms?.policies?.isNullOrEmpty() == false) {
|
||||||
|
span {
|
||||||
|
textStyle = "bold"
|
||||||
|
text = getString(R.string.settings_privacy_policy)
|
||||||
|
}
|
||||||
|
identityServerWithTerms.policies.forEach {
|
||||||
|
+"\n • "
|
||||||
|
// Use the url as the text too
|
||||||
|
link(it.url, it.url)
|
||||||
|
}
|
||||||
|
+"\n\n"
|
||||||
|
}
|
||||||
|
+getString(R.string.identity_server_consent_dialog_content_question)
|
||||||
|
}
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, configuredIdentityServer ?: ""))
|
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, identityServerWithTerms?.serverUrl.orEmpty()))
|
||||||
.setMessage(R.string.identity_server_consent_dialog_content_2)
|
.setMessage(content)
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.reactions_agree) { _, _ ->
|
||||||
consentCallBack.invoke()
|
consentCallBack.invoke()
|
||||||
}
|
}
|
||||||
.setNeutralButton(R.string.identity_server_consent_dialog_neutral_policy) { _, _ ->
|
.setNegativeButton(R.string.action_not_now, null)
|
||||||
policyLinkCallback.invoke()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.no, null)
|
|
||||||
.show()
|
.show()
|
||||||
|
.apply {
|
||||||
|
// Make the link(s) clickable. Must be called after show()
|
||||||
|
(findViewById(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,6 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
||||||
private fun clearNotifications() {
|
private fun clearNotifications() {
|
||||||
// Dismiss all notifications
|
// Dismiss all notifications
|
||||||
notificationDrawerManager.clearAllEvents()
|
notificationDrawerManager.clearAllEvents()
|
||||||
notificationDrawerManager.persistInfo()
|
|
||||||
|
|
||||||
// Also clear the dynamic shortcuts
|
// Also clear the dynamic shortcuts
|
||||||
shortcutsHandler.clearShortcuts()
|
shortcutsHandler.clearShortcuts()
|
||||||
|
|
|
@ -49,11 +49,11 @@ fun MultiPickerFileType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
|
fun MultiPickerAudioType.toContentAttachmentData(isVoiceMessage: Boolean): ContentAttachmentData {
|
||||||
if (mimeType == null) Timber.w("No mimeType")
|
if (mimeType == null) Timber.w("No mimeType")
|
||||||
return ContentAttachmentData(
|
return ContentAttachmentData(
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
type = mapType(),
|
type = if (isVoiceMessage) ContentAttachmentData.Type.VOICE_MESSAGE else mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
name = displayName,
|
name = displayName,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
|
@ -75,7 +75,7 @@ fun MultiPickerBaseType.toContentAttachmentData(): ContentAttachmentData {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is MultiPickerImageType -> toContentAttachmentData()
|
is MultiPickerImageType -> toContentAttachmentData()
|
||||||
is MultiPickerVideoType -> toContentAttachmentData()
|
is MultiPickerVideoType -> toContentAttachmentData()
|
||||||
is MultiPickerAudioType -> toContentAttachmentData()
|
is MultiPickerAudioType -> toContentAttachmentData(isVoiceMessage = false)
|
||||||
is MultiPickerFileType -> toContentAttachmentData()
|
is MultiPickerFileType -> toContentAttachmentData()
|
||||||
else -> throw IllegalStateException("Unknown file type")
|
else -> throw IllegalStateException("Unknown file type")
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,8 @@ class CallControlsView @JvmOverloads constructor(
|
||||||
views.ringingControlDecline.isVisible = true
|
views.ringingControlDecline.isVisible = true
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
}
|
}
|
||||||
|
CallState.CreateOffer,
|
||||||
|
CallState.Idle,
|
||||||
is CallState.Connected,
|
is CallState.Connected,
|
||||||
is CallState.Dialing,
|
is CallState.Dialing,
|
||||||
is CallState.Answering -> {
|
is CallState.Answering -> {
|
||||||
|
@ -105,7 +107,7 @@ class CallControlsView @JvmOverloads constructor(
|
||||||
views.videoToggleIcon.isVisible = state.isVideoCall
|
views.videoToggleIcon.isVisible = state.isVideoCall
|
||||||
views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED
|
views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED
|
||||||
}
|
}
|
||||||
else -> {
|
is CallState.Ended -> {
|
||||||
views.ringingControls.isVisible = false
|
views.ringingControls.isVisible = false
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,5 +21,6 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
sealed class ContactsBookAction : VectorViewModelAction {
|
sealed class ContactsBookAction : VectorViewModelAction {
|
||||||
data class FilterWith(val filter: String) : ContactsBookAction()
|
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||||
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||||
|
object UserConsentRequest : ContactsBookAction()
|
||||||
object UserConsentGranted : ContactsBookAction()
|
object UserConsentGranted : ContactsBookAction()
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue