diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1ba71c1f61..14b5112818 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,8 +25,8 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -49,8 +49,8 @@ jobs:
if: github.ref == 'refs/heads/main'
# Only runs on main, no concurrency.
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index ee4a87293f..4e701faa44 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -7,5 +7,5 @@ jobs:
runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time.
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 277809a58f..7f789b4610 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -13,53 +13,7 @@ env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
- -PallWarningsAsErrors=false
-
jobs:
- # Build Android Tests [Matrix SDK]
- build-android-test-matrix-sdk:
- name: Matrix SDK - Build Android Tests
- runs-on: macos-latest
- # No concurrency required, runs every time on a schedule.
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-java@v2
- with:
- distribution: 'adopt'
- java-version: 11
- - uses: actions/cache@v2
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
- - name: Build Android Tests for matrix-sdk-android
- run: ./gradlew clean matrix-sdk-android:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
-
- # Build Android Tests [Matrix APP]
- build-android-test-app:
- name: App - Build Android Tests
- runs-on: macos-latest
- # No concurrency required, runs every time on a schedule.
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-java@v2
- with:
- distribution: 'adopt'
- java-version: 11
- - uses: actions/cache@v2
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
- - name: Build Android Tests for vector
- run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
-
# Run Android Tests
integration-tests:
name: Matrix SDK - Running Integration Tests
@@ -70,7 +24,7 @@ jobs:
api-level: [ 28 ]
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
with:
@@ -80,7 +34,7 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: 3.8
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -89,11 +43,11 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
- run: |
- pip install matrix-synapse
- curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
- chmod 777 start.sh
- ./start.sh --no-rate-limit
+ uses: michaelkaye/setup-matrix-synapse@v0.4.0
+ with:
+ uploadLogs: true
+ httpPort: 8080
+ disableRateLimiting: true
# package: org.matrix.android.sdk.session
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
uses: reactivecircus/android-emulator-runner@v2
@@ -104,11 +58,20 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator-session.log
+ chmod 777 emulator-session.log
+ adb logcat >> emulator-session.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.session]
if: always()
id: get-comment-body-session
run: python3 ./tools/ci/render_test_output.py session ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
+ - name: Remove adb logcat
+ if: always()
+ run: pkill -9 adb
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.account] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -119,11 +82,20 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.account' matrix-sdk-android:connectedDebugAndroidTest
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator-account.log
+ chmod 777 emulator-account.log
+ adb logcat >> emulator-account.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.account' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.account]
if: always()
id: get-comment-body-account
run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
+ - name: Remove adb logcat
+ if: always()
+ run: pkill -9 adb
# package: org.matrix.android.sdk.internal
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}]
if: always()
@@ -135,11 +107,20 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.internal' matrix-sdk-android:connectedDebugAndroidTest
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator-internal.log
+ chmod 777 emulator-internal.log
+ adb logcat >> emulator-internal.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.internal' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.internal]
if: always()
id: get-comment-body-internal
run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
+ - name: Remove adb logcat
+ if: always()
+ run: pkill -9 adb
# package: org.matrix.android.sdk.ordering
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}]
if: always()
@@ -151,11 +132,20 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.ordering' matrix-sdk-android:connectedDebugAndroidTest
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator-ordering.log
+ chmod 777 emulator-ordering.log
+ adb logcat >> emulator-ordering.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.ordering' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.ordering]
if: always()
id: get-comment-body-ordering
run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
+ - name: Remove adb logcat
+ if: always()
+ run: pkill -9 adb
# package: class PermalinkParserTest
- name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}]
if: always()
@@ -167,11 +157,20 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
- script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class='org.matrix.android.sdk.PermalinkParserTest' matrix-sdk-android:connectedDebugAndroidTest
+ script: |
+ adb root
+ adb logcat -c
+ touch emulator-permalink.log
+ chmod 777 emulator-permalink.log
+ adb logcat >> emulator-permalink.log &
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class='org.matrix.android.sdk.PermalinkParserTest' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.PermalinkParserTest]
if: always()
id: get-comment-body-permalink
run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
+ - name: Remove adb logcat
+ if: always()
+ run: pkill -9 adb
# package: class PermalinkParserTest
- name: Find Comment
if: always() && github.event_name == 'pull_request'
@@ -196,6 +195,17 @@ jobs:
- `[org.matrix.android.sdk.ordering]`
${{ steps.get-comment-body-ordering.outputs.ordering }}
- `[org.matrix.android.sdk.PermalinkParserTest]`
${{ steps.get-comment-body-permalink.outputs.permalink }}
edit-mode: replace
+ - name: Upload Test Report Log
+ uses: actions/upload-artifact@v2
+ if: always()
+ with:
+ name: integrationtest-error-results
+ path: |
+ emulator-permalink.log
+ emulator-internal.log
+ emulator-ordering.log
+ emulator-account.log
+ emulator-session.log
ui-tests:
name: UI Tests (Synapse)
@@ -206,14 +216,12 @@ jobs:
api-level: [ 28 ]
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v2
- with:
- ref: develop
+ - uses: actions/checkout@v3
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.8
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -222,10 +230,11 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
- run: |
- pip install matrix-synapse
- curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
- | sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
+ uses: michaelkaye/setup-matrix-synapse@v0.4.0
+ with:
+ uploadLogs: true
+ httpPort: 8080
+ disableRateLimiting: true
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
@@ -245,22 +254,79 @@ jobs:
touch emulator.log
chmod 777 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 && exit 1 )
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
- name: Upload Test Report Log
uses: actions/upload-artifact@v2
if: always()
with:
- name: sanity-error-results
+ name: uitest-error-results
path: |
emulator.log
failure_screenshots/
+ codecov-units:
+ name: Unit tests with code coverage
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+ - run: ./gradlew allCodeCoverageReport $CI_GRADLE_ARG_PROPERTIES
+ - name: Upload Codecov data
+ uses: actions/upload-artifact@v2
+ if: always()
+ with:
+ name: codecov-xml
+ path: |
+ build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
+
+ sonarqube:
+ name: Sonarqube upload
+ runs-on: macos-latest
+ if: always()
+ needs:
+ - codecov-units
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+ - uses: actions/download-artifact@v3
+ with:
+ name: codecov-xml # will restore to allCodeCoverageReport.xml by default; we restore to the same location in following tasks
+ - run: mkdir -p build/reports/jacoco/allCodeCoverageReport/
+ - run: mv allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/
+ - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
+ env:
+ ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
+
# Notify the channel about scheduled runs, do not notify for manually triggered runs
notify:
+ name: Notify matrix
runs-on: ubuntu-latest
needs:
- integration-tests
- ui-tests
+ - sonarqube
if: always() && github.event_name != 'workflow_dispatch'
# No concurrency required, runs every time on a schedule.
steps:
@@ -270,4 +336,4 @@ jobs:
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
- html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
\ No newline at end of file
+ html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 02827e7f17..d427d65b7f 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -10,7 +10,7 @@ jobs:
name: Project Check Suite
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Run code quality check suite
run: ./tools/check/check_code_quality.sh
@@ -23,7 +23,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Run ktlint
run: |
./gradlew ktlintCheck --continue
@@ -96,8 +96,8 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -129,8 +129,8 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml
index 55873c9112..d390c47696 100644
--- a/.github/workflows/sync-from-external-sources.yml
+++ b/.github/workflows/sync-from-external-sources.yml
@@ -11,7 +11,7 @@ jobs:
if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
@@ -38,7 +38,7 @@ jobs:
if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
@@ -64,7 +64,7 @@ jobs:
if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule.
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Run analytics import script
run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d6e194916b..98e5f588ca 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,6 +12,30 @@ env:
-Porg.gradle.parallel=false
jobs:
+ # Build Android Tests
+ build-android-tests:
+ name: Build Android Tests
+ runs-on: ubuntu-latest
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: 11
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+ - name: Build Android Tests
+ run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
+
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
@@ -20,8 +44,8 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - uses: actions/checkout@v3
+ - uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -30,7 +54,10 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Run unit tests
- run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace
+ run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES --stacktrace
+ - name: Format unit test results
+ if: always()
+ run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always() &&
@@ -38,3 +65,20 @@ jobs:
( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
with:
files: ./**/build/test-results/**/*.xml
+
+# Notify the channel about runs against develop or main that have failures, as PRs should have caught these first.
+ notify:
+ runs-on: ubuntu-latest
+ needs:
+ - unit-tests
+ - build-android-tests
+ if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) && failure() }}
+ steps:
+ - uses: michaelkaye/matrix-hookshot-action@v0.3.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
+ matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
+ text_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}{{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
+ html_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion }} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
+
diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml
index 4a786a9339..1cbf29cc8d 100644
--- a/.github/workflows/update-gradle-wrapper.yml
+++ b/.github/workflows/update-gradle-wrapper.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1
diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index ed572b573f..85290e72df 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -11,6 +11,7 @@
emoji
emojis
fdroid
+ ganfra
gplay
hmac
homeserver
@@ -18,6 +19,7 @@
ktlint
linkified
linkify
+ manu
megolm
msisdn
msisdns
diff --git a/CHANGES.md b/CHANGES.md
index 318290107a..2a97120452 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,120 @@
+Changes in Element v1.4.8 (2022-03-28)
+======================================
+
+Other changes
+-------------
+ - Moving live location sharing permission to debug only builds whilst it is WIP ([#5636](https://github.com/vector-im/element-android/issues/5636))
+
+
+Changes in Element v1.4.7 (2022-03-24)
+======================================
+
+Bugfixes 🐛
+----------
+ - Fix inconsistencies between the arrow visibility and the collapse action on the room sections ([#5616](https://github.com/vector-im/element-android/issues/5616))
+ - Fix room list header count flickering
+
+Changes in Element v1.4.6 (2022-03-23)
+======================================
+
+Features ✨
+----------
+ - Thread timeline is now live and much faster especially for large or old threads ([#5230](https://github.com/vector-im/element-android/issues/5230))
+ - View all threads per room screen is now live when the home server supports threads ([#5232](https://github.com/vector-im/element-android/issues/5232))
+ - Add a custom view to display a picker for share location options ([#5395](https://github.com/vector-im/element-android/issues/5395))
+ - Add ability to pin a location on map for sharing ([#5417](https://github.com/vector-im/element-android/issues/5417))
+ - Poll Integration Tests ([#5522](https://github.com/vector-im/element-android/issues/5522))
+ - Live location sharing: adding build config field and show permission dialog ([#5536](https://github.com/vector-im/element-android/issues/5536))
+ - Live location sharing: Adding indicator view when enabled ([#5571](https://github.com/vector-im/element-android/issues/5571))
+
+Bugfixes 🐛
+----------
+ - Poll system notifications on Android are not user friendly ([#4780](https://github.com/vector-im/element-android/issues/4780))
+ - Add colors for shield vector drawable ([#4860](https://github.com/vector-im/element-android/issues/4860))
+ - Support both stable and unstable prefixes for Events about Polls and Location ([#5340](https://github.com/vector-im/element-android/issues/5340))
+ - Fix missing messages when loading messages forwards ([#5448](https://github.com/vector-im/element-android/issues/5448))
+ - Fix presence indicator being aligned to the center of the room image ([#5489](https://github.com/vector-im/element-android/issues/5489))
+ - Read receipt in wrong order ([#5514](https://github.com/vector-im/element-android/issues/5514))
+ - Fix mentions using matrix.to rather than client defined permalink base url ([#5521](https://github.com/vector-im/element-android/issues/5521))
+ - Fixes crash when tapping the timeline verification surround box instead of the buttons ([#5540](https://github.com/vector-im/element-android/issues/5540))
+ - [Notification mode] Wrong mode is displayed when the mention only is selected on the web client ([#5547](https://github.com/vector-im/element-android/issues/5547))
+ - Fix local echos not being shown when re-opening rooms ([#5551](https://github.com/vector-im/element-android/issues/5551))
+ - Fix crash when closing a room while decrypting timeline events ([#5552](https://github.com/vector-im/element-android/issues/5552))
+ - Fix sometimes read marker not properly updating ([#5564](https://github.com/vector-im/element-android/issues/5564))
+
+In development 🚧
+----------------
+ - Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities ([#5375](https://github.com/vector-im/element-android/issues/5375))
+ - Introduces FTUE personalisation complete screen along with confetti celebration ([#5389](https://github.com/vector-im/element-android/issues/5389))
+
+SDK API changes ⚠️
+------------------
+ - Adds support for MSC3440, additional threads homeserver capabilities ([#5271](https://github.com/vector-im/element-android/issues/5271))
+
+Other changes
+-------------
+ - Refactoring for safer olm and megolm session usage ([#5380](https://github.com/vector-im/element-android/issues/5380))
+ - Improve headers UI in Rooms/Messages lists ([#4533](https://github.com/vector-im/element-android/issues/4533))
+ - Number of unread messages on space badge now include number of unread DMs ([#5260](https://github.com/vector-im/element-android/issues/5260))
+ - Amend spaces menu to be consistent with iOS version ([#5270](https://github.com/vector-im/element-android/issues/5270))
+ - Selected space highlight changed in left panel ([#5346](https://github.com/vector-im/element-android/issues/5346))
+ - [Rooms list] Do not suggest collapse the unique section ([#5347](https://github.com/vector-im/element-android/issues/5347))
+ - Add analytics support for threads ([#5378](https://github.com/vector-im/element-android/issues/5378))
+ - Add top margin before our first message ([#5384](https://github.com/vector-im/element-android/issues/5384))
+ - Improved onboarding registration unit test coverage ([#5408](https://github.com/vector-im/element-android/issues/5408))
+ - Adds stable room hierarchy endpoint with a fallback to the unstable one ([#5443](https://github.com/vector-im/element-android/issues/5443))
+ - Use ColorPrimary for attachmentGalleryButton tint ([#5501](https://github.com/vector-im/element-android/issues/5501))
+ - Added online presence indicator attribute online to match offline styling ([#5513](https://github.com/vector-im/element-android/issues/5513))
+ - Add a presence sync enabling build config ([#5563](https://github.com/vector-im/element-android/issues/5563))
+ - Show stickers on click ([#5572](https://github.com/vector-im/element-android/issues/5572))
+
+
+Changes in Element v1.4.4 (2022-03-09)
+======================================
+
+Features ✨
+----------
+ - Adds animated typing indicator to the bottom of the timeline ([#3296](https://github.com/vector-im/element-android/issues/3296))
+ - Removes the topic and typing information from the room's top bar ([#4642](https://github.com/vector-im/element-android/issues/4642))
+ - Add possibility to save media from Gallery + reorder choices in message context menu ([#5005](https://github.com/vector-im/element-android/issues/5005))
+ - Improves settings error dialog messaging when changing avatar or display name fails ([#5418](https://github.com/vector-im/element-android/issues/5418))
+
+Bugfixes 🐛
+----------
+ - Open direct message screen when clicking on DM button in the space members list ([#4319](https://github.com/vector-im/element-android/issues/4319))
+ - Fix incorrect media cache size in settings ([#5394](https://github.com/vector-im/element-android/issues/5394))
+ - Setting an avatar when creating a room had no effect ([#5402](https://github.com/vector-im/element-android/issues/5402))
+ - Fix reactions summary crash when reopening a room ([#5463](https://github.com/vector-im/element-android/issues/5463))
+ - Fixing room titles overlapping the room image in the room toolbar ([#5468](https://github.com/vector-im/element-android/issues/5468))
+
+In development 🚧
+----------------
+ - Starts the FTUE account personalisation flow by adding an account created screen behind a feature flag ([#5158](https://github.com/vector-im/element-android/issues/5158))
+
+SDK API changes ⚠️
+------------------
+ - Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive. ([#5330](https://github.com/vector-im/element-android/issues/5330))
+
+Other changes
+-------------
+ - Improve Bubble layouts rendering ([#5303](https://github.com/vector-im/element-android/issues/5303))
+ - Continue improving realm usage (potentially helping with storage and RAM usage) ([#5330](https://github.com/vector-im/element-android/issues/5330))
+ - Update reaction button layout. ([#5313](https://github.com/vector-im/element-android/issues/5313))
+ - Adds forceLoginFallback feature flag and usages to FTUE login and registration ([#5325](https://github.com/vector-im/element-android/issues/5325))
+ - Override task affinity to prevent unknown activities running in our app tasks. ([#4498](https://github.com/vector-im/element-android/issues/4498))
+ - Tentatively fixing the UI sanity test being unable to click on the space menu items ([#5269](https://github.com/vector-im/element-android/issues/5269))
+ - Moves attachment-viewer, diff-match-patch, and multipicker modules to subfolders under library ([#5309](https://github.com/vector-im/element-android/issues/5309))
+ - Log the `since` token used and `next_batch` token returned when doing an incremental sync. ([#5312](https://github.com/vector-im/element-android/issues/5312), [#5318](https://github.com/vector-im/element-android/issues/5318))
+ - Upgrades material dependency version from 1.4.0 to 1.5.0 ([#5392](https://github.com/vector-im/element-android/issues/5392))
+ - Using app name instead of hardcoded "Element" for exported keys filename ([#5326](https://github.com/vector-im/element-android/issues/5326))
+ - Upgrade the plugin which generate strings with template from 1.2.2 to 2.0.0 ([#5348](https://github.com/vector-im/element-android/issues/5348))
+ - Remove about 700 unused strings and their translations ([#5352](https://github.com/vector-im/element-android/issues/5352))
+ - Creates dedicated VectorOverrides for forcing behaviour for local testing/development ([#5361](https://github.com/vector-im/element-android/issues/5361))
+ - Cleanup unused threads build configurations ([#5379](https://github.com/vector-im/element-android/issues/5379))
+ - Notify element-android channel each time a nightly build completes. ([#5314](https://github.com/vector-im/element-android/issues/5314))
+ - Iterate on badge / unread indicator color ([#5456](https://github.com/vector-im/element-android/issues/5456))
+
+
Changes in Element v1.4.2 (2022-02-22 Palindrome Day!)
======================================================
diff --git a/build.gradle b/build.gradle
index 9cae9e7e70..31416a0440 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ buildscript {
classpath libs.gradle.hiltPlugin
classpath 'com.google.gms:google-services:4.3.10'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
- classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4'
+ classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.0.0"
// NOTE: Do not place your application dependencies here; they belong
@@ -105,11 +105,21 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
+def launchTask = getGradle()
+ .getStartParameter()
+ .getTaskRequests()
+ .toString()
+ .toLowerCase()
+
+if (launchTask.contains("codeCoverageReport".toLowerCase())) {
+ apply from: 'coverage.gradle'
+}
+
apply plugin: 'org.sonarqube'
// To run a sonar analysis:
// Run './gradlew sonarqube -Dsonar.login='
-// The SONAR_KEY is stored in passbolt
+// The SONAR_KEY is stored in passbolt as Token Sonar Cloud Bma
sonarqube {
properties {
@@ -119,10 +129,12 @@ sonarqube {
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8"
property "sonar.links.homepage", "https://github.com/vector-im/element-android/"
- property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android"
+ property "sonar.links.ci", "https://github.com/vector-im/element-android/actions"
property "sonar.links.scm", "https://github.com/vector-im/element-android/"
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization"
+ property "sonar.java.coveragePlugin", "jacoco"
+ property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
}
}
diff --git a/changelog.d/3296.bugfix b/changelog.d/3296.bugfix
deleted file mode 100644
index e5f8799f21..0000000000
--- a/changelog.d/3296.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Typing notifications moved from the header to the bottom of the timeline.
\ No newline at end of file
diff --git a/changelog.d/4319.bugfix b/changelog.d/4319.bugfix
deleted file mode 100644
index da42c864c6..0000000000
--- a/changelog.d/4319.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Open direct message screen when clicking on DM button in the space members list
diff --git a/changelog.d/5005.feature b/changelog.d/5005.feature
deleted file mode 100644
index ce3b2ad1f9..0000000000
--- a/changelog.d/5005.feature
+++ /dev/null
@@ -1 +0,0 @@
-Add possibility to save media from Gallery + reorder choices in message context menu
diff --git a/changelog.d/5158.wip b/changelog.d/5158.wip
deleted file mode 100644
index 67a3d83a7a..0000000000
--- a/changelog.d/5158.wip
+++ /dev/null
@@ -1 +0,0 @@
-Starts the FTUE account personalisation flow by adding an account created screen behind a feature flag
\ No newline at end of file
diff --git a/changelog.d/5269.misc b/changelog.d/5269.misc
deleted file mode 100644
index 699ddfd3dd..0000000000
--- a/changelog.d/5269.misc
+++ /dev/null
@@ -1 +0,0 @@
-Tentatively fixing the UI sanity test being unable to click on the space menu items
\ No newline at end of file
diff --git a/changelog.d/5303.misc b/changelog.d/5303.misc
deleted file mode 100644
index dbad0b738d..0000000000
--- a/changelog.d/5303.misc
+++ /dev/null
@@ -1 +0,0 @@
-Improve Bubble layouts rendering.
\ No newline at end of file
diff --git a/changelog.d/5309.misc b/changelog.d/5309.misc
deleted file mode 100644
index 83771995af..0000000000
--- a/changelog.d/5309.misc
+++ /dev/null
@@ -1 +0,0 @@
-Moves attachment-viewer, diff-match-patch, and multipicker modules to subfolders under library
\ No newline at end of file
diff --git a/changelog.d/5312.misc b/changelog.d/5312.misc
deleted file mode 100644
index d724f1ba3f..0000000000
--- a/changelog.d/5312.misc
+++ /dev/null
@@ -1 +0,0 @@
-Log the `since` token used and `next_batch` token returned when doing an incremental sync.
diff --git a/changelog.d/5313.misc b/changelog.d/5313.misc
deleted file mode 100644
index efc225a0a4..0000000000
--- a/changelog.d/5313.misc
+++ /dev/null
@@ -1 +0,0 @@
-Update reaction button layout.
\ No newline at end of file
diff --git a/changelog.d/5314.misc b/changelog.d/5314.misc
deleted file mode 100644
index 35fed08a61..0000000000
--- a/changelog.d/5314.misc
+++ /dev/null
@@ -1 +0,0 @@
-Notify element-android channel each time a nightly build completes.
diff --git a/changelog.d/5318.misc b/changelog.d/5318.misc
deleted file mode 100644
index d724f1ba3f..0000000000
--- a/changelog.d/5318.misc
+++ /dev/null
@@ -1 +0,0 @@
-Log the `since` token used and `next_batch` token returned when doing an incremental sync.
diff --git a/changelog.d/5325.feature b/changelog.d/5325.feature
deleted file mode 100644
index 23754c790d..0000000000
--- a/changelog.d/5325.feature
+++ /dev/null
@@ -1 +0,0 @@
-Adds forceLoginFallback feature flag and usages to FTUE login and registration
\ No newline at end of file
diff --git a/changelog.d/5326.misc b/changelog.d/5326.misc
deleted file mode 100644
index 5ffa732d53..0000000000
--- a/changelog.d/5326.misc
+++ /dev/null
@@ -1 +0,0 @@
-[Export e2ee keys] use appName instead of element
\ No newline at end of file
diff --git a/changelog.d/5330.misc b/changelog.d/5330.misc
deleted file mode 100644
index 6315ad536c..0000000000
--- a/changelog.d/5330.misc
+++ /dev/null
@@ -1 +0,0 @@
-Continue improving realm usage.
\ No newline at end of file
diff --git a/changelog.d/5330.sdk b/changelog.d/5330.sdk
deleted file mode 100644
index 3f6d46401c..0000000000
--- a/changelog.d/5330.sdk
+++ /dev/null
@@ -1 +0,0 @@
-Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive.
\ No newline at end of file
diff --git a/changelog.d/5348.misc b/changelog.d/5348.misc
deleted file mode 100644
index f5ee8627ce..0000000000
--- a/changelog.d/5348.misc
+++ /dev/null
@@ -1 +0,0 @@
-Upgrade the plugin which generate strings with template from 1.2.2 to 2.0.0
\ No newline at end of file
diff --git a/changelog.d/5352.misc b/changelog.d/5352.misc
deleted file mode 100644
index 956de682d8..0000000000
--- a/changelog.d/5352.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove about 700 unused strings and their translations
\ No newline at end of file
diff --git a/changelog.d/5379.misc b/changelog.d/5379.misc
deleted file mode 100644
index d485636f10..0000000000
--- a/changelog.d/5379.misc
+++ /dev/null
@@ -1 +0,0 @@
-Cleanup unused threads build configurations
\ No newline at end of file
diff --git a/changelog.d/5392.misc b/changelog.d/5392.misc
deleted file mode 100644
index 54d7dba992..0000000000
--- a/changelog.d/5392.misc
+++ /dev/null
@@ -1 +0,0 @@
-Upgrades material dependency version from 1.4.0 to 1.5.0
diff --git a/changelog.d/5394.bugfix b/changelog.d/5394.bugfix
deleted file mode 100644
index f8c5311492..0000000000
--- a/changelog.d/5394.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix incorrect media cache size in settings
\ No newline at end of file
diff --git a/changelog.d/5426.feature b/changelog.d/5426.feature
new file mode 100644
index 0000000000..2dee22f07a
--- /dev/null
+++ b/changelog.d/5426.feature
@@ -0,0 +1 @@
+Allow scrolling position of Voice Message playback
\ No newline at end of file
diff --git a/changelog.d/5473.bugfix b/changelog.d/5473.bugfix
new file mode 100644
index 0000000000..e53329e202
--- /dev/null
+++ b/changelog.d/5473.bugfix
@@ -0,0 +1 @@
+Fixes polls being votable after being ended
diff --git a/changelog.d/5497.bugfix b/changelog.d/5497.bugfix
new file mode 100644
index 0000000000..4228ebaafb
--- /dev/null
+++ b/changelog.d/5497.bugfix
@@ -0,0 +1 @@
+[Subscribing] Blank display name
\ No newline at end of file
diff --git a/changelog.d/5517.misc b/changelog.d/5517.misc
new file mode 100644
index 0000000000..18269afcc6
--- /dev/null
+++ b/changelog.d/5517.misc
@@ -0,0 +1 @@
+Flattening the asynchronous onboarding state and passing all errors through the same pipeline
\ No newline at end of file
diff --git a/changelog.d/5595.feature b/changelog.d/5595.feature
new file mode 100644
index 0000000000..8fd4d4b144
--- /dev/null
+++ b/changelog.d/5595.feature
@@ -0,0 +1 @@
+Live Location Sharing - Foreground Service and Notification
\ No newline at end of file
diff --git a/coverage.gradle b/coverage.gradle
new file mode 100644
index 0000000000..96881dfff2
--- /dev/null
+++ b/coverage.gradle
@@ -0,0 +1,55 @@
+def excludes = [ ]
+
+def initializeReport(report, projects, classExcludes) {
+ projects.each { project -> project.apply plugin: 'jacoco' }
+ report.executionData { fileTree(rootProject.rootDir.absolutePath).include("**/build/jacoco/*.exec") }
+
+ report.reports {
+ xml.enabled true
+ html.enabled true
+ csv.enabled false
+ }
+
+ gradle.projectsEvaluated {
+ def androidSourceDirs = []
+ def androidClassDirs = []
+
+ projects.each { project ->
+ switch (project) {
+ case { project.plugins.hasPlugin("com.android.application") }:
+ androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug")
+ androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
+ androidSourceDirs.add("${project.projectDir}/src/main/java")
+ break
+ case { project.plugins.hasPlugin("com.android.library") }:
+ androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug")
+ androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
+ androidSourceDirs.add("${project.projectDir}/src/main/java")
+ break
+ default:
+ report.sourceSets project.sourceSets.main
+ }
+ }
+
+ report.sourceDirectories.setFrom(report.sourceDirectories + files(androidSourceDirs))
+ def classFiles = androidClassDirs.collect { files(it).files }.flatten()
+ report.classDirectories.setFrom(files((report.classDirectories.files + classFiles).collect {
+ fileTree(dir: it, excludes: classExcludes)
+ }))
+ }
+}
+
+def collectProjects(predicate) {
+ return subprojects.findAll { it.buildFile.isFile() && predicate(it) }
+}
+
+task allCodeCoverageReport(type: JacocoReport) {
+ outputs.upToDateWhen { false }
+ rootProject.apply plugin: 'jacoco'
+ // to limit projects in a specific report, add
+ // def excludedProjects = [ ... ]
+ // def projects = collectProjects { !excludedProjects.contains(it.name) }
+ def projects = collectProjects { true }
+ dependsOn { projects*.test }
+ initializeReport(it, projects, excludes)
+}
diff --git a/dependencies.gradle b/dependencies.gradle
index 87b8e3c12f..7666a3bf9f 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -9,13 +9,13 @@ ext.versions = [
def gradle = "7.0.4"
// Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.5.31"
-def kotlinCoroutines = "1.5.2"
+def kotlin = "1.6.0"
+def kotlinCoroutines = "1.6.0"
def dagger = "2.40.5"
def retrofit = "2.9.0"
def arrow = "0.8.2"
def markwon = "4.6.2"
-def moshi = "1.12.0"
+def moshi = "1.13.0"
def lifecycle = "2.4.0"
def flowBinding = "1.2.0"
def epoxy = "4.6.2"
@@ -58,6 +58,7 @@ ext.libs = [
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
+ 'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
'datastore' : "androidx.datastore:datastore:1.0.0",
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
@@ -141,4 +142,4 @@ ext.libs = [
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
'junit' : "junit:junit:4.13.2"
]
-]
\ No newline at end of file
+]
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 7de8100469..45883f506d 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -29,9 +29,10 @@ ext.groups = [
'com\\.google\\.android\\..*',
],
group: [
- 'com.google.firebase',
'com.android',
'com.android.tools',
+ 'com.google.firebase',
+ 'com.google.testing.platform',
]
],
mavenCentral: [
@@ -63,6 +64,8 @@ ext.groups = [
'com.github.piasy',
'com.github.shyiko.klob',
'com.google',
+ 'com.google.android',
+ 'com.google.api.grpc',
'com.google.auto.service',
'com.google.auto.value',
'com.google.code.findbugs',
@@ -111,10 +114,13 @@ ext.groups = [
'io.arrow-kt',
'io.github.detekt.sarif4k',
'io.github.reactivecircus.flowbinding',
+ 'io.grpc',
'io.jsonwebtoken',
'io.kindedj',
'io.mockk',
+ 'io.netty',
'io.noties.markwon',
+ 'io.opencensus',
'io.reactivex.rxjava2',
'io.realm',
'it.unimi.dsi',
@@ -150,6 +156,7 @@ ext.groups = [
'org.ec4j.core',
'org.glassfish.jaxb',
'org.hamcrest',
+ 'org.jacoco',
'org.jetbrains',
'org.jetbrains.intellij.deps',
'org.jetbrains.kotlin',
@@ -175,6 +182,7 @@ ext.groups = [
'org.sonatype.oss',
'org.testng',
'org.threeten',
+ 'org.webjars',
'ru.noties',
'xerces',
'xml-apis',
diff --git a/fastlane/metadata/android/de-DE/changelogs/40104000.txt b/fastlane/metadata/android/de-DE/changelogs/40104000.txt
new file mode 100644
index 0000000000..37de3cb4a2
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Neues: Erstelle Threads, damit dein Chatverlauf nicht zugespammt wird. Nachrichtenblasen.
+Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/de-DE/changelogs/40104020.txt b/fastlane/metadata/android/de-DE/changelogs/40104020.txt
new file mode 100644
index 0000000000..6693401a24
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Neues: Unterstützung für @room, Verbesserungen der Abstimmungen und weitere kleine Änderungen
+Ganzer Changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/en-US/changelogs/40104040.txt b/fastlane/metadata/android/en-US/changelogs/40104040.txt
new file mode 100644
index 0000000000..d36b10c390
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104040.txt
@@ -0,0 +1,2 @@
+Main changes in this version: typing indicator UI updates. Various bug fixes and stability improvements.
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.4
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40104060.txt b/fastlane/metadata/android/en-US/changelogs/40104060.txt
new file mode 100644
index 0000000000..1863bef5fb
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104060.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements.
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.6
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40104070.txt b/fastlane/metadata/android/en-US/changelogs/40104070.txt
new file mode 100644
index 0000000000..99a3ecfe7b
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104070.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Various bug fixes and stability improvements.
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.7
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40104080.txt b/fastlane/metadata/android/en-US/changelogs/40104080.txt
new file mode 100644
index 0000000000..66ed1664bd
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104080.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Thread timeline are now live and faster. Various bug fixes and stability improvements.
+Full changelog: https://github.com/vector-im/element-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/es-ES/changelogs/40104000.txt b/fastlane/metadata/android/es-ES/changelogs/40104000.txt
new file mode 100644
index 0000000000..ea607fe19a
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Principales cambios de esta versión: primera implementación de los hilos de mensajes. Burbujas de mensajes.
+Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/es-ES/changelogs/40104020.txt b/fastlane/metadata/android/es-ES/changelogs/40104020.txt
new file mode 100644
index 0000000000..8c2c78cb62
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Principales cambios de esta versión: añadir @room, encuestas cerradas y muchos cambios menores más.
+Todos los cambios en: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/fa/changelogs/40104000.txt b/fastlane/metadata/android/fa/changelogs/40104000.txt
new file mode 100644
index 0000000000..7beb79981f
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+تغییرات اصلی در این نگارش: پیاده سازی نخستین پیامهای رشتهای. حبابهای پیام.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/fa/changelogs/40104020.txt b/fastlane/metadata/android/fa/changelogs/40104020.txt
new file mode 100644
index 0000000000..6d5148220d
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+تغییرات اصلی در این نگارش: افزودن پشتیبانی به @room و نظرسنجیهای فاش نشده در کنار تغییرات کوچک دیگر.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103170.txt b/fastlane/metadata/android/fr-FR/changelogs/40103170.txt
new file mode 100644
index 0000000000..c264ea3703
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40103170.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.17
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103180.txt b/fastlane/metadata/android/fr-FR/changelogs/40103180.txt
new file mode 100644
index 0000000000..0b8a9542a5
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40103180.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.18
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104000.txt b/fastlane/metadata/android/fr-FR/changelogs/40104000.txt
new file mode 100644
index 0000000000..eaced9e3f4
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Implémentation initial des fils de discussion. Bulles de messages.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104020.txt b/fastlane/metadata/android/fr-FR/changelogs/40104020.txt
new file mode 100644
index 0000000000..068b4aac43
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Ajout du support pour @room et des sondages non terminé parmi plein d'autres changements mineurs.
+Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt
index 899b4cd978..b43613eb20 100644
--- a/fastlane/metadata/android/hu-HU/full_description.txt
+++ b/fastlane/metadata/android/hu-HU/full_description.txt
@@ -1,43 +1,43 @@
-Element egy biztonságos üzenetküldő és csapatmunka támogató alkalmazás ami ideális távoli munkavégzés közben csoportos csevegéshez. Az alkalmazás végpontok közötti titkosítást használ videó konferenciához, fájl megosztáshoz és videó hivásokhoz.
+Az Element egy biztonságos üzenetküldő, és egy csapatmunka app, amely távoli munkavégzéshez is alkalmas lehet. Az alkalmazás végponti titkosítás használatával biztosít videó konferencia, fájlmegosztás, és audio hívás lehetőségeket.
-Element tulajdonságai:
-- Fejlett online kommunikációs eszköz
-- Teljesen titkosított üzenetküldés biztonságos céges kommunikációt kínál még a távdolgozóknak is
-- Elosztott csevegés a Matrix nyílt forráskódú keretrendszer felhasználásával
-- Bizontságos fájl megosztás titkosítottan projektek kezeléséhez
-- Videó hívás VoIP-pal és képernyőmegosztással
-- Könnyen integrálható a kedvenc online kollaborációs eszközöddel, projekt menedzsment eszközzel, VoIP szolgáltatással vagy más csoport üzenetküldő alkalmazással
+Az Element funkciói többek között:
+- Fejlett online kommunikációs eszközök
+- Titkosított üzenetek a biztonságos céges kommunikációhoz, otthonról dolgozóknak is
+- Decentralizált chat a nyílt forráskódú Matrix protokoll használatával
+- Biztonságos fájlmegosztáss a projektek kezeléséhez
+- Videochat, VoIP, és képernyőmegosztási lehetőséggel
+- Egyszerű integráció a kedvenc online kollaborációs eszközeiddel, projektkezelési eszközökkel, VoIP szolgáltatásokkal, és más csoportos üzenetküldő alkalmazásokkal
-Element teljesen más mint a többi üzenetküldő alkalmazás. Matrixot használ, egy nyílt hálózatot a decentralizált biztonságos kommunikációhoz. Lehetőséget ad saját szerver üzemeltetésére ami maximális tulajdont és kontrollt biztosít az adatok fölött.
+Az Element teljesen más, mint az összes többi üzenetküldő és kollaborációs alkalmazás. A biztonságos üzenetküldést és decentralizált kommunikációt biztosító Matrix platformot használja. Akár egyénileg üzemeltetett szervereket is lehet használni az adatok teljes kontrollálása érdekében.
-Magánélet védelme és titkosított üzenetküldés
-Element megóv a kéretlen hirdetésektől, adatbányászattól és a különböző szigetszerű megoldásoktól. Minden adatot biztonságba helyez, egy az egybe videó és hang kommunikáció végpontok között titkosítva ahol az eszközök hitelesítve vannak.
+Magánszféra és titkosított csevegés
+Az Element megvéd a nemkívánatos hirdetésektől, adatbányászattól, és a zárt platformoktól. Ezeken felül biztonságban tartja az összes adatod és 1:1 hívásod a végponti titkosításnak és az eszközök-közti hitelesítésnek köszönhetően.
-Element a kezedbe adja az adatvédelmi irányítást miközben bárkivel kommunikálhatsz a Matrix hálózatban vagy más üzleti kollaborációs eszközzel ami integrálva van, mint amilyen a Slack.
+Az Element átadja neked az irányítást a magánszférád felett, miközben lehetővé teszi, hogy biztonságosan kommunikálj bárkivel a Matrix hálózatban, vagy a többi üzleti kommunikációs eszközt használókkal, az olyan appok integrálásának köszönhetően, mint például a Slack.
-Element futtatható saját szerveren
-Azért, hogy az érzékeny adatok és beszélgetések minnél inkább az irányításod alatt lehessen az Elementet saját magadnak üzemeltetheted vagy választhatsz bármely Matrixon alapuló - szabványos nyílt forráskódú és decentralizált kommunikáció - szoláltató közül. Element adatvédelmet, biztonságot és rugalmas integrációkat biztosít.
+Element can be self-hosted
+To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility.
-A te adatod a tiéd
-Te döntöd el, hogy hol tárolod az adataidat és üzeneteidet. Adatbányászat vagy harmadik fél hozzáférésének kockázata nélkül.
+Own your data
+You decide where to keep your data and messages. Without the risk of data mining or access from third parties.
-Element többféle képpen adja vissza az irányítást:
-1. Szerezz egy ingyenes hozzáférést a matrix.org nyilvános szerverre amit a Matrix fejlesztők üzemeltetnek vagy válassz a több ezer önkéntesek által üzemeltetett nyilvános szerverből
-2. Üzemeltess szerver magadnak a saját infrastruktúrádon
-3. Iratkozz fel egy egyedi szerverre az Element Matrix Services platformon
+Element puts you in control in different ways:
+1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers
+2. Self-host your account by running a server on your own IT infrastructure
+3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform
-Nyílt üzenetküldés és kollaboráció
-Bárkivel beszélgethetsz a Matrix hálózaton, akár az Elementet használja akár egy másik Matrix alkalmazást használ vagy akár egy eltérő üzenetküldőt.
+Open messaging and collaboration
+You can chat with anyone on the Matrix network, whether they’re using Element, another Matrix app or even if they are using a different messaging app.
-Fantasztikusan biztonságos
-Igazi végpontok között titkosítás (csak a beszélgetésben résztvevők tudják visszafejteni) és hitelesítés eszközök közötti aláírásokkal.
+Super secure
+Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signed device verification.
-Teljes kommunikáció és integráció
-Üzenetküldés, hang és videóhívás, fájl megosztás, képernyő megosztás és egy csomó integráció, botok és kisalkalmazások. Építs szobákat, közösségeket, maradj kapcsolatban és végezz el dolgokat.
+Complete communication and integration
+Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.
-Vedd fel a fonalat
-Maradj kapcsolatban bárhol minden eszközödön a szinkronizált üzenetekkel és a weben a https://app.element.io oldallal
+Pick up where you left off
+Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io
-Nyílt forráskód
-Element Android egy nyílt forráskódú projekt a GitHubon. Küldj hibajegyet és/vagy vegyél részt a fejlesztésében itt: https://github.com/vector-im/element-android
+Open source
+Element Android is an open source project, hosted by GitHub. Please report bugs and/or contribute to its development at https://github.com/vector-im/element-android
diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt
index 2dfe14c516..51be689331 100644
--- a/fastlane/metadata/android/hu-HU/short_description.txt
+++ b/fastlane/metadata/android/hu-HU/short_description.txt
@@ -1 +1 @@
-Csoportos üzenetküldő - titkosított üzenetek, videó hívások
+Csoportos üzenetküldő - titkosított üzenetek és videó hívások
diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt
index 907f907f99..c463dea393 100644
--- a/fastlane/metadata/android/hu-HU/title.txt
+++ b/fastlane/metadata/android/hu-HU/title.txt
@@ -1 +1 @@
-Element
+Element - Biztonságos üzenetküldő
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt
index 48af96d216..0f9fc720a9 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100100.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100100.txt
@@ -1,2 +1,2 @@
今回の新バージョンでは、主にバグの修正と改善が行われています。メッセージの送信がより速くなりました。
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.10
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.10
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100110.txt b/fastlane/metadata/android/ja-JP/changelogs/40100110.txt
index b8b9798fcd..d67486a147 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100110.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100110.txt
@@ -1,2 +1,2 @@
今回の新バージョンでは、主にUI(ユーザーインターフェース)とUX(ユーザーエクスペリエンス)の向上が図られています。友達を招待したり、QRコードを読み取って素早くDMを作成できるようになりました。
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.11
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.11
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100120.txt b/fastlane/metadata/android/ja-JP/changelogs/40100120.txt
index 01c33c5d52..1e10e5f2e3 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100120.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100120.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.12
+このバージョンの主な変更点:URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.12
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100130.txt b/fastlane/metadata/android/ja-JP/changelogs/40100130.txt
index 941a052239..0e5ef9b8eb 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100130.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100130.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.13
+このバージョンの主な変更点:URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.13
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt
index 6dc536cdcf..8fa9848d0b 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100140.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100140.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: 部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.14
+このバージョンの主な変更点:部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.14
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100150.txt b/fastlane/metadata/android/ja-JP/changelogs/40100150.txt
index caded1b8ed..c94330b70b 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100150.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100150.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: ソーシャルログインに対応しました。
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15
+このバージョンの主な変更点:ソーシャルログインに対応しました。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt
index 1b1a2092b0..ae947f1781 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100160.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100160.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: パフォーマンスの向上とバグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16
+このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt
index a0cc7b107d..01b742a9a2 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40100170.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40100170.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: バグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.17
+このバージョンの主な変更点:バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.17
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt
index d0900f38c2..0c09cee3dd 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40101000.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101000.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: パフォーマンスの向上とバグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.0
+このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.0
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt
index cb204e5696..25ac73b449 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40101010.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101010.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: パフォーマンスの向上とバグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.1
+このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.1
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt
index bb6ab66525..762879a281 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40101020.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101020.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: パフォーマンスの向上とバグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.2
+このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.2
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt
index e7ecc05a0f..3c641c09ac 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40101030.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101030.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点: パフォーマンスの向上とバグの修正!
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.3
+このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました!
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.3
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101040.txt b/fastlane/metadata/android/ja-JP/changelogs/40101040.txt
new file mode 100644
index 0000000000..2dc1cdb781
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101040.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:パフォーマンスの向上と不具合の修正
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.4
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101100.txt b/fastlane/metadata/android/ja-JP/changelogs/40101100.txt
new file mode 100644
index 0000000000..2f720498ec
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101100.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:テーマ、スタイルの更新と、スペースに関する新機能。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.10
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101160.txt b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt
index 985ea10510..3e37e353d7 100644
--- a/fastlane/metadata/android/ja-JP/changelogs/40101160.txt
+++ b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt
@@ -1,2 +1,2 @@
-このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。
-全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.16
+このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.16
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103070.txt b/fastlane/metadata/android/ja-JP/changelogs/40103070.txt
new file mode 100644
index 0000000000..09c44e990d
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103070.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:主に通知に関する不具合の修正。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103080.txt b/fastlane/metadata/android/ja-JP/changelogs/40103080.txt
new file mode 100644
index 0000000000..7c37f5a756
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103080.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:不具合の修正
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.8
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103090.txt b/fastlane/metadata/android/ja-JP/changelogs/40103090.txt
new file mode 100644
index 0000000000..580b49e6d9
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103090.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:音声メッセージの下書き機能の追加。不具合の修正。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.9
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103100.txt b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt
new file mode 100644
index 0000000000..0527756005
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103100.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:アンケート機能のサポート(実験的)。URL プレビューの新規デザイン。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.10
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103110.txt b/fastlane/metadata/android/ja-JP/changelogs/40103110.txt
new file mode 100644
index 0000000000..5295af5833
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103110.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:不具合の修正
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.11
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103120.txt b/fastlane/metadata/android/ja-JP/changelogs/40103120.txt
new file mode 100644
index 0000000000..3859bee8d5
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103120.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:不具合の修正
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.12
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103130.txt b/fastlane/metadata/android/ja-JP/changelogs/40103130.txt
new file mode 100644
index 0000000000..19d04a9b99
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103130.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.13
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103140.txt b/fastlane/metadata/android/ja-JP/changelogs/40103140.txt
new file mode 100644
index 0000000000..c9f5062c5b
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103140.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.14
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40103150.txt b/fastlane/metadata/android/ja-JP/changelogs/40103150.txt
new file mode 100644
index 0000000000..89c3117cf5
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40103150.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:登録時の表示に関する変更(Analyticsへのオプトインなど)。数学に関するイベントをラボに追加。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.3.15
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104000.txt b/fastlane/metadata/android/ja-JP/changelogs/40104000.txt
new file mode 100644
index 0000000000..22a205dc37
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:スレッド機能の実装、吹き出しメッセージ。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/ja-JP/changelogs/40104020.txt b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt
new file mode 100644
index 0000000000..e792008faf
--- /dev/null
+++ b/fastlane/metadata/android/ja-JP/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+このバージョンの主な変更点:@roomの対応、非公開の投票など。
+更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt
index 6014938cce..ce1550acb0 100644
--- a/fastlane/metadata/android/ja-JP/full_description.txt
+++ b/fastlane/metadata/android/ja-JP/full_description.txt
@@ -1,42 +1,42 @@
-Elementは、安全なメッセンジャー、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンドツーエンドの暗号化を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。
+Elementは、安全なメッセージングアプリ、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンド・ツー・エンドの暗号化技術を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。
Elementの特徴
- 高度なオンラインコミュニケーションツール
-- 完全に暗号化されたメッセージにより、リモートワーカーでも、より安全な企業コミュニケーションが可能
-- Matrixオープンソースフレームワークをベースにした分散型のチャット
-- プロジェクトを管理しながら、暗号化されたデータで安全にファイル共有
+- メッセージの完全な暗号化。リモートワーカーでも、より安全な企業コミュニケーションが可能
+- Matrixオープンソースフレームワークに基づく、分散型のチャット
+- プロジェクトの管理と並行して、データの暗号化によりファイルを安全に共有することが可能
- Voice over IPによるビデオチャットと画面共有
-- お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能
+- お気に入りのオンラインコラボレーションツールや、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能
-Elementは他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権)コミュニケーションのためのオープンネットワークであるMatrixで動作します。ユーザーが自分のデータやメッセージを最大限にコントロールできるように、セルフホスティングも可能です。
+Elementは、他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権型)コミュニケーションのためのオープンネットワークであるMatrixで動作します。自分のデータやメッセージを最大限にコントロールするために、あなた自身がサーバーを運営することもできます。
プライバシーと暗号化されたコミュニケーション
-Elementは、望ましくない広告、データマイニング、ウォールドガーデンからユーザーを保護します。また、エンド・ツー・エンドの暗号化と相互署名された端末の検証により、全てのデータ、1対1のビデオおよび音声通信を保護します。
+Elementは、望ましくない広告、データマイニング、囲い込みからユーザーを守ります。また、エンド・ツー・エンドの暗号化と、相互署名による端末の認証に基づき、全てのデータ、ビデオ会議、音声通信を保護します。
-Elementは、Slackなどのアプリと統合することで、Matrixネットワーク上の誰とでも安全にコミュニケーションを取ることができると同時に、プライバシーをコントロールすることができます。
+Elementでは、Matrixネットワークにいる誰とでもコミュニケーションが行えるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うとともに、プライバシーをコントロールすることができます。
-Elementはセルフホスティングが可能
-機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixベースのホストを選択することもできます。Elementは、プライバシー、セキュリティーコンプライアンス、および統合の柔軟性を提供します。
+セルフホスティングが可能
+機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixに基づくサーバーを選ぶこともできます。Elementは、プライバシー、セキュリティーコンプライアンス、および柔軟な機能統合を提供します。
自分のデータを所有する
-データやメッセージをどこに保管するかは、ユーザー自身が決めることができます。データマイニングやサードパーティからのアクセスのリスクはありません。
+データやメッセージを保管する場所を自分で決めることができます。データマイニングや第三者へのデータ流出のリスクはありません。
-Elementでは、どのサーバーを使うかを、ご自身で決めることができます。
-1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得するか、ボランティアがホストしているパブリックサーバーから選択する。
+Elementでは、どのサーバーを使うかをご自身で決めることができます。
+1. 開発者が運営する matrix.org の公開サーバーで無料アカウントを取得するか、ボランティアが管理している運営サーバーから選ぶ。
2. あなた自身がサーバーを運営し、アカウントを管理する。
-3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。
+3. Element Matrix Servicesの運営プラットフォームに加入し、カスタムサーバー上でアカウントを作る。
オープンなメッセージングとコラボレーション
-Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらには他のメッセージングアプリを使っているかに関わらず、チャットをすることができます。
+相手がElement、他のMatrixアプリ、さらには他のメッセージングアプリを使っているかに関わらず、Matrixネットワーク上の誰とでもチャットをすることができます。
非常に安全
-本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できる)と、相互署名された端末の検証を行います。
+本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、クロス署名による端末の認証が可能です。
包括的なコミュニケーションと統合
-メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くのインテグレーション、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます。
+メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。
-中断からの再開
-メッセージの履歴は全ての端末とウェブ(https://app.element.io)で完全に同期されるので、どこからでも連絡を取り合うことができます。
+いつでも、どこにいても
+メッセージの履歴は、全ての端末とウェブ(https://app.element.io)で完全に同期されるので、どこからでも連絡を取り合うことができます。
オープンソース
-Element AndroidはGitHubで開発されているオープンソースのプロジェクトです。 バグの報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。
+Element Androidは、GitHubで開発されているオープンソースのプロジェクトです。 不具合の報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104000.txt b/fastlane/metadata/android/ru-RU/changelogs/40104000.txt
new file mode 100644
index 0000000000..f6bf34b3cc
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: Начальная реализация веток сообщений. Сообщения пузыри.
+Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104020.txt b/fastlane/metadata/android/ru-RU/changelogs/40104020.txt
new file mode 100644
index 0000000000..864bd03d5e
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Основные изменения в этой версии: добавлена поддержка @room и нераскрытых опросов, а также множество других мелких изменений.
+Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/sq/changelogs/40104000.txt b/fastlane/metadata/android/sq/changelogs/40104000.txt
new file mode 100644
index 0000000000..f917c7c0cb
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Ndryshime kryesore në këtë version: Sendërtimi fillestar i mesazheve në rrjedha. Flluska mesazhesh.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/sq/changelogs/40104020.txt b/fastlane/metadata/android/sq/changelogs/40104020.txt
new file mode 100644
index 0000000000..2fbe4f2bf6
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: shtim mbulimi për @room dhe për pyetësorë jopublikë, mes mjaft ndryshimesh të tjera të vockla.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104000.txt b/fastlane/metadata/android/sv-SE/changelogs/40104000.txt
new file mode 100644
index 0000000000..6bce52ba36
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40104000.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Initial implementation av trådmeddelanden. Meddelandebubblor.
+Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.0
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104020.txt b/fastlane/metadata/android/sv-SE/changelogs/40104020.txt
new file mode 100644
index 0000000000..e3b5d4cd1c
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40104020.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: lägg till stöd för @room och slutna omröstningar, och många andra små ändringar.
+Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.2
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index dcf5e2cb7b..db3bccc1f9 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
+distributionSha256Sum=a9a7b7baba105f6557c9dcf9c3c6e8f7e57e6b49889c5f1d133f015d0727e4be
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle
index 15f46754b3..0cad8ac171 100644
--- a/library/jsonviewer/build.gradle
+++ b/library/jsonviewer/build.gradle
@@ -59,7 +59,7 @@ dependencies {
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
- testImplementation 'org.json:json:20211205'
+ testImplementation 'org.json:json:20220320'
testImplementation libs.tests.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espressoCore
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
index 96b5a9c997..9f8093f801 100644
--- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
@@ -20,13 +20,12 @@ import android.content.Context
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
-import com.airbnb.mvrx.Success
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.Span
import me.gujun.android.span.span
internal class JSonViewerEpoxyController(private val context: Context) :
- TypedEpoxyController() {
+ TypedEpoxyController() {
private var styleProvider: JSonViewerStyleProvider = JSonViewerStyleProvider.default(context)
@@ -44,10 +43,8 @@ internal class JSonViewerEpoxyController(private val context: Context) :
text(async.error.localizedMessage?.toEpoxyCharSequence())
}
}
- is Success -> {
- val model = data.root.invoke()
-
- model?.let {
+ else -> {
+ async.invoke()?.let {
buildRec(it, 0, "")
}
}
@@ -55,9 +52,9 @@ internal class JSonViewerEpoxyController(private val context: Context) :
}
private fun buildRec(
- model: JSonViewerModel,
- depth: Int,
- idBase: String
+ model: JSonViewerModel,
+ depth: Int,
+ idBase: String
) {
val host = this
val id = "$idBase/${model.key ?: model.index}_${model.isExpanded}}"
@@ -74,34 +71,34 @@ internal class JSonViewerEpoxyController(private val context: Context) :
id(id + "_sum")
depth(depth)
text(
- span {
- if (model.key != null) {
- span("\"${model.key}\"") {
- textColor = host.styleProvider.keyColor
- }
- span(" : ") {
- textColor = host.styleProvider.baseColor
- }
- }
- if (model.index != null) {
- span("${model.index}") {
- textColor = host.styleProvider.secondaryColor
- }
- span(" : ") {
- textColor = host.styleProvider.baseColor
- }
- }
span {
- +"{+${model.keys.size}}"
- textColor = host.styleProvider.baseColor
- }
- }.toEpoxyCharSequence()
+ if (model.key != null) {
+ span("\"${model.key}\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ if (model.index != null) {
+ span("${model.index}") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ span {
+ +"{+${model.keys.size}}"
+ textColor = host.styleProvider.baseColor
+ }
+ }.toEpoxyCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(model) })
}
}
}
- is JSonViewerArray -> {
+ is JSonViewerArray -> {
if (model.isExpanded) {
open(id, model.key, model.index, depth, false, model)
model.items.forEach {
@@ -113,6 +110,38 @@ internal class JSonViewerEpoxyController(private val context: Context) :
id(id + "_sum")
depth(depth)
text(
+ span {
+ if (model.key != null) {
+ span("\"${model.key}\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ if (model.index != null) {
+ span("${model.index}") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ span {
+ +"[+${model.items.size}]"
+ textColor = host.styleProvider.baseColor
+ }
+ }.toEpoxyCharSequence()
+ )
+ itemClickListener(View.OnClickListener { host.itemClicked(model) })
+ }
+ }
+ }
+ is JSonViewerLeaf -> {
+ valueItem {
+ id(id)
+ depth(depth)
+ text(
span {
if (model.key != null) {
span("\"${model.key}\"") {
@@ -122,6 +151,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
textColor = host.styleProvider.baseColor
}
}
+
if (model.index != null) {
span("${model.index}") {
textColor = host.styleProvider.secondaryColor
@@ -130,41 +160,8 @@ internal class JSonViewerEpoxyController(private val context: Context) :
textColor = host.styleProvider.baseColor
}
}
- span {
- +"[+${model.items.size}]"
- textColor = host.styleProvider.baseColor
- }
+ append(host.valueToSpan(model))
}.toEpoxyCharSequence()
- )
- itemClickListener(View.OnClickListener { host.itemClicked(model) })
- }
- }
- }
- is JSonViewerLeaf -> {
- valueItem {
- id(id)
- depth(depth)
- text(
- span {
- if (model.key != null) {
- span("\"${model.key}\"") {
- textColor = host.styleProvider.keyColor
- }
- span(" : ") {
- textColor = host.styleProvider.baseColor
- }
- }
-
- if (model.index != null) {
- span("${model.index}") {
- textColor = host.styleProvider.secondaryColor
- }
- span(" : ") {
- textColor = host.styleProvider.baseColor
- }
- }
- append(host.valueToSpan(model))
- }.toEpoxyCharSequence()
)
copyValue(model.stringRes)
}
@@ -175,12 +172,12 @@ internal class JSonViewerEpoxyController(private val context: Context) :
private fun valueToSpan(leaf: JSonViewerLeaf): Span {
val host = this
return when (leaf.type) {
- JSONType.STRING -> {
+ JSONType.STRING -> {
span("\"${leaf.stringRes}\"") {
textColor = host.styleProvider.stringColor
}
}
- JSONType.NUMBER -> {
+ JSONType.NUMBER -> {
span(leaf.stringRes) {
textColor = host.styleProvider.numberColor
}
@@ -190,7 +187,7 @@ internal class JSonViewerEpoxyController(private val context: Context) :
textColor = host.styleProvider.booleanColor
}
}
- JSONType.NULL -> {
+ JSONType.NULL -> {
span("null") {
textColor = host.styleProvider.booleanColor
}
@@ -199,42 +196,42 @@ internal class JSonViewerEpoxyController(private val context: Context) :
}
private fun open(
- id: String,
- key: String?,
- index: Int?,
- depth: Int,
- isObject: Boolean = true,
- composed: JSonViewerModel
+ id: String,
+ key: String?,
+ index: Int?,
+ depth: Int,
+ isObject: Boolean = true,
+ composed: JSonViewerModel
) {
val host = this
valueItem {
id("${id}_Open")
depth(depth)
text(
- span {
- if (key != null) {
- span("\"$key\"") {
- textColor = host.styleProvider.keyColor
+ span {
+ if (key != null) {
+ span("\"$key\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
}
- span(" : ") {
- textColor = host.styleProvider.baseColor
+ if (index != null) {
+ span("$index") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
}
- }
- if (index != null) {
- span("$index") {
+ span("- ") {
textColor = host.styleProvider.secondaryColor
}
- span(" : ") {
+ span("{".takeIf { isObject } ?: "[") {
textColor = host.styleProvider.baseColor
}
- }
- span("- ") {
- textColor = host.styleProvider.secondaryColor
- }
- span("{".takeIf { isObject } ?: "[") {
- textColor = host.styleProvider.baseColor
- }
- }.toEpoxyCharSequence()
+ }.toEpoxyCharSequence()
)
itemClickListener(View.OnClickListener { host.itemClicked(composed) })
}
@@ -251,10 +248,10 @@ internal class JSonViewerEpoxyController(private val context: Context) :
id("${id}_Close")
depth(depth)
text(
- span {
- text = "}".takeIf { isObject } ?: "]"
- textColor = host.styleProvider.baseColor
- }.toEpoxyCharSequence()
+ span {
+ text = "}".takeIf { isObject } ?: "]"
+ textColor = host.styleProvider.baseColor
+ }.toEpoxyCharSequence()
)
}
}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
index 227ac2a71d..00d66645e6 100644
--- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
@@ -20,7 +20,6 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.view.ContextMenu
-import android.view.Menu
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
@@ -77,10 +76,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder() {
menuInfo: ContextMenu.ContextMenuInfo?
) {
if (copyValue != null) {
- val menuItem = menu?.add(
- Menu.NONE, R.id.copy_value,
- Menu.NONE, R.string.copy_value
- )
+ val menuItem = menu?.add(R.string.copy_value)
val clipService =
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
menuItem?.setOnMenuItemClickListener {
diff --git a/library/jsonviewer/src/main/res/menu/jv_menu_item.xml b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
deleted file mode 100644
index 4da69b5117..0000000000
--- a/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/library/jsonviewer/src/main/res/values/strings.xml b/library/jsonviewer/src/main/res/values/strings.xml
index cc4b8726b4..fbd67256f5 100644
--- a/library/jsonviewer/src/main/res/values/strings.xml
+++ b/library/jsonviewer/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
+
Copy Value
diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
index 87cf48d0a7..72880babbf 100644
--- a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
+++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/CursorExtensions.kt
@@ -17,7 +17,12 @@
package im.vector.lib.multipicker.utils
import android.database.Cursor
+import androidx.core.database.getStringOrNull
fun Cursor.getColumnIndexOrNull(column: String): Int? {
return getColumnIndex(column).takeIf { it != -1 }
}
+
+fun Cursor.readStringColumnOrNull(column: String): String? {
+ return getColumnIndexOrNull(column)?.let { getStringOrNull(it) }
+}
diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle
index cee58414c7..0ac513b252 100644
--- a/library/ui-styles/build.gradle
+++ b/library/ui-styles/build.gradle
@@ -60,6 +60,4 @@ dependencies {
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
// dialpad dimen
implementation 'im.dlg:android-dialer:1.2.5'
- // AudioRecordView attr
- implementation 'com.github.Armen101:AudioRecordView:1.0.5'
}
\ No newline at end of file
diff --git a/library/ui-styles/src/debug/res/layout/activity_debug_button_styles.xml b/library/ui-styles/src/debug/res/layout/activity_debug_button_styles.xml
index 0f129fb406..cc15bb1b3b 100644
--- a/library/ui-styles/src/debug/res/layout/activity_debug_button_styles.xml
+++ b/library/ui-styles/src/debug/res/layout/activity_debug_button_styles.xml
@@ -71,19 +71,6 @@
android:enabled="false"
android:text="Destructive disabled" />
-
-
-
-
-
-
-
-
diff --git a/library/ui-styles/src/main/res/color/button_social_google_background_selector_dark.xml b/library/ui-styles/src/main/res/color/button_social_google_background_selector_dark.xml
deleted file mode 100644
index 3893ce3e34..0000000000
--- a/library/ui-styles/src/main/res/color/button_social_google_background_selector_dark.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values-land/styles_dial_pad.xml b/library/ui-styles/src/main/res/values-land/styles_dial_pad.xml
deleted file mode 100644
index 39c5bf9aa6..0000000000
--- a/library/ui-styles/src/main/res/values-land/styles_dial_pad.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index 770b001893..d887e7774e 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -9,10 +9,6 @@
#2f9edb
?colorError
-
- #14368BD6
- @color/palette_azure
-
@color/palette_azure
@color/palette_melon
@@ -22,7 +18,6 @@
#99000000
#27303A
- #FF61708B
#1E61708B
@@ -56,6 +51,12 @@
+
+
+
@android:color/white
@@ -77,16 +78,6 @@
#BF000000
#BF000000
-
- #FFFFFFFF
- #FF22262E
- #FF090A0C
-
-
- #FFE9EDF1
- #FF22262E
- #FF090A0C
-
#EBEFF5
#27303A
@@ -101,9 +92,7 @@
#AAAAAAAA
#55555555
-
#EEEEEE
- #61708B
#FFF3F8FD
@@ -133,4 +122,18 @@
@color/palette_gray_100
@color/palette_gray_450
+
+ @color/palette_element_green
+ @color/palette_element_green
+
+
+
+ @color/palette_prune
+ @color/palette_prune
+
+
+ #0DBD8B
+ #17191C
+ #FF4B55
+
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index be57f75dc8..600c73c878 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -9,20 +9,11 @@
32dp
50dp
- 16dp
- 196dp
- 44dp
- 72dp
16dp
32dp
- 40dp
- 60dp
-
- 4dp
8dp
- 8dp
- 0.75
@@ -67,4 +58,13 @@
- 0.01
- 0.35
-
\ No newline at end of file
+
+ - 0.15
+ - 0.05
+
+
+ 10dp
+ 16dp
+ 12dp
+ 8dp
+
diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml
index e37fd8a7c6..e6cee80b59 100644
--- a/library/ui-styles/src/main/res/values/palette.xml
+++ b/library/ui-styles/src/main/res/values/palette.xml
@@ -1,8 +1,12 @@
-
+
-
+
#368BD6
@@ -15,6 +19,7 @@
#0DBD8B
#FFFFFF
#FF5B55
+
#7E69FF
#2DC2C5
#5C56F5
@@ -27,6 +32,7 @@
#8D97A5
#737D8C
#17191C
+
#F4F9FD
diff --git a/library/ui-styles/src/main/res/values/palette_mobile.xml b/library/ui-styles/src/main/res/values/palette_mobile.xml
index c22b9705c7..5610771f8a 100644
--- a/library/ui-styles/src/main/res/values/palette_mobile.xml
+++ b/library/ui-styles/src/main/res/values/palette_mobile.xml
@@ -35,7 +35,6 @@
@color/palette_gray_25
@color/palette_black_950
- @color/palette_black_950
@color/palette_white
@color/palette_black_800
@@ -54,5 +53,4 @@
@color/palette_verde
@color/palette_azure
@color/palette_grape
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml b/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml
new file mode 100644
index 0000000000..f2c703764a
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/stylable_location_sharing_option_picker_view.xml b/library/ui-styles/src/main/res/values/stylable_location_sharing_option_picker_view.xml
new file mode 100644
index 0000000000..25b2687fed
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_location_sharing_option_picker_view.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml b/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml
new file mode 100644
index 0000000000..a7c45918af
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/style_action_button.xml b/library/ui-styles/src/main/res/values/style_action_button.xml
new file mode 100644
index 0000000000..0a3c73622f
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/style_action_button.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_attachments.xml b/library/ui-styles/src/main/res/values/styles_attachments.xml
deleted file mode 100644
index 18c2e3f95f..0000000000
--- a/library/ui-styles/src/main/res/values/styles_attachments.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml
index d09d0a399d..004aca5aaa 100644
--- a/library/ui-styles/src/main/res/values/styles_buttons.xml
+++ b/library/ui-styles/src/main/res/values/styles_buttons.xml
@@ -33,24 +33,6 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/styles_dial_pad.xml b/library/ui-styles/src/main/res/values/styles_dial_pad.xml
index 34e128c56d..77dc0b3081 100644
--- a/library/ui-styles/src/main/res/values/styles_dial_pad.xml
+++ b/library/ui-styles/src/main/res/values/styles_dial_pad.xml
@@ -1,5 +1,8 @@
-
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_social_login.xml b/library/ui-styles/src/main/res/values/styles_social_login.xml
index 5a76f70f2e..810ce14c6b 100644
--- a/library/ui-styles/src/main/res/values/styles_social_login.xml
+++ b/library/ui-styles/src/main/res/values/styles_social_login.xml
@@ -28,11 +28,6 @@
- @color/black_54
-
-
diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml
index 2e87353303..81d2e7581d 100644
--- a/library/ui-styles/src/main/res/values/styles_voice_message.xml
+++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml
@@ -2,14 +2,14 @@
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/theme_black.xml b/library/ui-styles/src/main/res/values/theme_black.xml
index 44d4206d43..6e5ce80c19 100644
--- a/library/ui-styles/src/main/res/values/theme_black.xml
+++ b/library/ui-styles/src/main/res/values/theme_black.xml
@@ -11,8 +11,6 @@
- @color/vctr_fab_label_stroke_black
- @color/vctr_fab_label_color_black
- @color/vctr_touch_guard_bg_black
- - @color/vctr_attachment_selector_background_black
- - @color/vctr_attachment_selector_border_black
- @color/vctr_room_active_widgets_banner_bg_black
- @color/vctr_room_active_widgets_banner_text_black
- @color/vctr_reaction_background_off_black
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index 100a07f41d..7177687fdd 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -21,8 +21,6 @@
- @color/vctr_fab_label_color_dark
- @color/vctr_touch_guard_bg_dark
- @color/vctr_keys_backup_banner_accent_color_dark
- - @color/vctr_attachment_selector_background_dark
- - @color/vctr_attachment_selector_border_dark
- @color/vctr_room_active_widgets_banner_bg_dark
- @color/vctr_room_active_widgets_banner_text_dark
- @color/vctr_reaction_background_off_dark
@@ -45,12 +43,14 @@
- @color/vctr_presence_indicator_offline_dark
+ - @color/vctr_presence_indicator_online_dark
-
+
- ?vctr_system
- ?vctr_content_quinary
- ?vctr_system
- ?vctr_system
+ - ?vctr_content_tertiary
- @color/element_accent_dark
@@ -141,6 +141,11 @@
- @style/Widget.Vector.Keyword
- @color/vctr_toast_background_dark
+
+ - @style/Widget.Vector.ActionButton
+
+
+ - @color/vctr_live_location_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 39e78ee5b1..c90c021591 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -21,8 +21,6 @@
- @color/vctr_fab_label_color_light
- @color/vctr_touch_guard_bg_light
- @color/vctr_keys_backup_banner_accent_color_light
- - @color/vctr_attachment_selector_background_light
- - @color/vctr_attachment_selector_border_light
- @color/vctr_room_active_widgets_banner_bg_light
- @color/vctr_room_active_widgets_banner_text_light
- @color/vctr_reaction_background_off_light
@@ -45,12 +43,14 @@
- @color/vctr_presence_indicator_offline_light
+ - @color/vctr_presence_indicator_online_light
-
+
- ?vctr_system
- ?vctr_content_quinary
- ?vctr_system
- ?vctr_system
+ - ?vctr_content_tertiary
- @color/element_accent_light
@@ -142,6 +142,11 @@
- @style/Widget.Vector.Keyword
- @color/vctr_toast_background_light
+
+ - @style/Widget.Vector.ActionButton
+
+
+ - @color/vctr_live_location_light
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index c5d1d19fec..d737715306 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.session.room.send.UserDraft
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
@@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
return room.getLiveRoomNotificationState().asFlow()
}
+ fun liveThreadSummaries(): Flow> {
+ return room.getAllThreadSummariesLive().asFlow()
+ .startWith(room.coroutineDispatchers.io) {
+ room.getAllThreadSummaries()
+ }
+ }
fun liveThreadList(): Flow> {
return room.getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getAllThreads()
}
}
-
fun liveLocalUnreadThreadList(): Flow> {
return room.getMarkedThreadNotificationsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 32a8b23f30..1e2eda166f 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -31,12 +31,11 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- buildConfigField "String", "SDK_VERSION", "\"1.4.4\""
+ buildConfigField "String", "SDK_VERSION", "\"1.4.8\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
- resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
- resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
- resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
+ buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
+ buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\""
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
@@ -167,7 +166,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45'
testImplementation libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7.3'
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 031d0a8bcf..ac4ccf56d1 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -71,7 +71,7 @@ class CommonTestHelper(context: Context) {
)
)
}
- matrix = TestMatrix.getInstance(context)
+ matrix = TestMatrix.getInstance()
}
fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
index 5c9b79361e..0f79896b2c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
@@ -23,7 +23,7 @@ object TestConstants {
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
// Time out to use when waiting for server response.
- private const val AWAIT_TIME_OUT_MILLIS = 30_000
+ private const val AWAIT_TIME_OUT_MILLIS = 60_000
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt
index e92232a7c5..fa44167a8f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt
@@ -105,16 +105,9 @@ internal class TestMatrix constructor(context: Context, matrixConfiguration: Mat
}
}
- fun getInstance(context: Context): TestMatrix {
- if (isInit.compareAndSet(false, true)) {
- val appContext = context.applicationContext
- if (appContext is MatrixConfiguration.Provider) {
- val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
- instance = TestMatrix(appContext, matrixConfiguration)
- } else {
- throw IllegalStateException("Matrix is not initialized properly." +
- " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
- }
+ fun getInstance(): TestMatrix {
+ if (isInit.compareAndSet(false, false)) {
+ throw IllegalStateException("Matrix is not initialized properly. You should call TestMatrix.initialize first")
}
return instance
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
new file mode 100644
index 0000000000..41ec69cdc5
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -0,0 +1,649 @@
+/*
+ * Copyright 2022 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.crypto
+
+import android.util.Log
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.delay
+import org.amshove.kluent.fail
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.TestMatrixCallback
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
+import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeSanityTests : InstrumentedTest {
+
+ private val testHelper = CommonTestHelper(context())
+ private val cryptoTestHelper = CryptoTestHelper(testHelper)
+
+ /**
+ * Simple test that create an e2ee room.
+ * Some new members are added, and a message is sent.
+ * We check that the message is e2e and can be decrypted.
+ *
+ * Additional users join, we check that they can't decrypt history
+ *
+ * Alice sends a new message, then check that the new one can be decrypted
+ */
+ @Test
+ fun testSendingE2EEMessages() {
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = cryptoTestData.firstSession
+ val e2eRoomID = cryptoTestData.roomId
+
+ val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+ // add some more users and invite them
+ val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
+ .map {
+ testHelper.createAccount(it, SessionTestParams(true))
+ }
+
+ Log.v("#E2E TEST", "All accounts created")
+ // we want to invite them in the room
+ otherAccounts.forEach {
+ testHelper.runBlockingTest {
+ Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+ aliceRoomPOV.invite(it.myUserId)
+ }
+ }
+
+ // All user should accept invite
+ otherAccounts.forEach { otherSession ->
+ waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
+ Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
+ }
+
+ // check that alice see them as joined (not really necessary?)
+ ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
+
+ Log.v("#E2E TEST", "All users have joined the room")
+ Log.v("#E2E TEST", "Alice is sending the message")
+
+ val text = "This is my message"
+ val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
+ // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
+ Assert.assertTrue("Message should be sent", sentEventId != null)
+
+ // All should be able to decrypt
+ otherAccounts.forEach { otherSession ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ }
+
+ // Add a new user to the room, and check that he can't decrypt
+ val newAccount = listOf("adam") // , "adam", "manu")
+ .map {
+ testHelper.createAccount(it, SessionTestParams(true))
+ }
+
+ newAccount.forEach {
+ testHelper.runBlockingTest {
+ Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+ aliceRoomPOV.invite(it.myUserId)
+ }
+ }
+
+ newAccount.forEach {
+ waitForAndAcceptInviteInRoom(it, e2eRoomID)
+ }
+
+ ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
+
+ // wait a bit
+ testHelper.runBlockingTest {
+ delay(3_000)
+ }
+
+ // check that messages are encrypted (uisi)
+ newAccount.forEach { otherSession ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
+ Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+ }
+ timelineEvent != null &&
+ timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
+ timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
+ }
+ }
+ }
+
+ // Let alice send a new message
+ Log.v("#E2E TEST", "Alice sends a new message")
+
+ val secondMessage = "2 This is my message"
+ val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
+
+ // new members should be able to decrypt it
+ newAccount.forEach { otherSession ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
+ Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+ }
+ timelineEvent != null &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE &&
+ secondMessage == timelineEvent.root.getClearContent().toModel()?.body
+ }
+ }
+ }
+
+ otherAccounts.forEach {
+ testHelper.signOutAndClose(it)
+ }
+ newAccount.forEach { testHelper.signOutAndClose(it) }
+
+ cryptoTestData.cleanUp(testHelper)
+ }
+
+ /**
+ * Quick test for basic key backup
+ * 1. Create e2e between Alice and Bob
+ * 2. Alice sends 3 messages, using 3 different sessions
+ * 3. Ensure bob can decrypt
+ * 4. Create backup for bob and upload keys
+ *
+ * 5. Sign out alice and bob to ensure no gossiping will happen
+ *
+ * 6. Let bob sign in with a new session
+ * 7. Ensure history is UISI
+ * 8. Import backup
+ * 9. Check that new session can decrypt
+ */
+ @Test
+ fun testBasicBackupImport() {
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession!!
+ val e2eRoomID = cryptoTestData.roomId
+
+ Log.v("#E2E TEST", "Create and start key backup for bob ...")
+ val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
+ val keyBackupPassword = "FooBarBaz"
+ val megolmBackupCreationInfo = testHelper.doSync {
+ bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
+ }
+ val version = testHelper.doSync {
+ bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
+ }
+ Log.v("#E2E TEST", "... Key backup started and enabled for bob")
+ // Bob session should now have
+
+ val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+ // let's send a few message to bob
+ val sentEventIds = mutableListOf()
+ val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
+ messagesText.forEach { text ->
+ val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+ sentEventIds.add(it)
+ }
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ // we want more so let's discard the session
+ aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
+
+ testHelper.runBlockingTest {
+ delay(1_000)
+ }
+ }
+ Log.v("#E2E TEST", "Bob received all and can decrypt")
+
+ // Let's wait a bit to be sure that bob has backed up the session
+
+ Log.v("#E2E TEST", "Force key backup for Bob...")
+ testHelper.waitWithLatch { latch ->
+ bobKeysBackupService.backupAllGroupSessions(
+ null,
+ TestMatrixCallback(latch, true)
+ )
+ }
+ Log.v("#E2E TEST", "... Key backup done for Bob")
+
+ // Now lets logout both alice and bob to ensure that we won't have any gossiping
+
+ val bobUserId = bobSession.myUserId
+ Log.v("#E2E TEST", "Logout alice and bob...")
+ testHelper.signOutAndClose(aliceSession)
+ testHelper.signOutAndClose(bobSession)
+ Log.v("#E2E TEST", "..Logout alice and bob...")
+
+ testHelper.runBlockingTest {
+ delay(1_000)
+ }
+
+ // Create a new session for bob
+ Log.v("#E2E TEST", "Create a new session for Bob")
+ val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
+
+ // check that bob can't currently decrypt
+ Log.v("#E2E TEST", "check that bob can't currently decrypt")
+ sentEventIds.forEach { sentEventId ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
+ Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
+ }
+ timelineEvent != null &&
+ timelineEvent.root.getClearType() == EventType.ENCRYPTED
+ }
+ }
+ }
+ // after initial sync events are not decrypted, so we have to try manually
+ ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+ // Let's now import keys from backup
+
+ newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
+ val keyVersionResult = testHelper.doSync {
+ keysBackupService.getVersion(version.version, it)
+ }
+
+ val importedResult = testHelper.doSync {
+ keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
+ keyBackupPassword,
+ null,
+ null,
+ null, it)
+ }
+
+ assertEquals(3, importedResult.totalNumberOfKeys)
+ }
+
+ // ensure bob can now decrypt
+ ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+ testHelper.signOutAndClose(newBobSession)
+ }
+
+ /**
+ * Check that a new verified session that was not supposed to get the keys initially will
+ * get them from an older one.
+ */
+ @Test
+ fun testSimpleGossip() {
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = cryptoTestData.firstSession
+ val bobSession = cryptoTestData.secondSession!!
+ val e2eRoomID = cryptoTestData.roomId
+
+ val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+ cryptoTestHelper.initializeCrossSigning(bobSession)
+
+ // let's send a few message to bob
+ val sentEventIds = mutableListOf()
+ val messagesText = listOf("1. Hello", "2. Bob")
+
+ Log.v("#E2E TEST", "Alice sends some messages")
+ messagesText.forEach { text ->
+ val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+ sentEventIds.add(it)
+ }
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ }
+
+ // Ensure bob can decrypt
+ ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
+
+ // Let's now add a new bob session
+ // Create a new session for bob
+ Log.v("#E2E TEST", "Create a new session for Bob")
+ val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
+
+ // check that new bob can't currently decrypt
+ Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+
+ ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+ // Try to request
+ sentEventIds.forEach { sentEventId ->
+ val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+ newBobSession.cryptoService().requestRoomKeyForEvent(event)
+ }
+
+ // wait a bit
+ testHelper.runBlockingTest {
+ delay(10_000)
+ }
+
+ // Ensure that new bob still can't decrypt (keys must have been withheld)
+ ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
+
+ // Now mark new bob session as verified
+
+ bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+ newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
+
+ // now let new session re-request
+ sentEventIds.forEach { sentEventId ->
+ val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+ newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
+ }
+
+ // wait a bit
+ testHelper.runBlockingTest {
+ delay(10_000)
+ }
+
+ ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+ cryptoTestData.cleanUp(testHelper)
+ testHelper.signOutAndClose(newBobSession)
+ }
+
+ /**
+ * Test that if a better key is forwarded (lower index, it is then used)
+ */
+ @Test
+ fun testForwardBetterKey() {
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = cryptoTestData.firstSession
+ val bobSessionWithBetterKey = cryptoTestData.secondSession!!
+ val e2eRoomID = cryptoTestData.roomId
+
+ val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+ cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
+
+ // let's send a few message to bob
+ var firstEventId: String
+ val firstMessage = "1. Hello"
+
+ Log.v("#E2E TEST", "Alice sends some messages")
+ firstMessage.let { text ->
+ firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ }
+
+ // Ensure bob can decrypt
+ ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
+
+ // Let's add a new unverified session from bob
+ val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
+
+ // check that new bob can't currently decrypt
+ Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+ ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+ // Now let alice send a new message. this time the new bob session will be able to decrypt
+ var secondEventId: String
+ val secondMessage = "2. New Device?"
+
+ Log.v("#E2E TEST", "Alice sends some messages")
+ secondMessage.let { text ->
+ secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ }
+
+ // check that both messages have same sessionId (it's just that we don't have index 0)
+ val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+ val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+
+ val firstSessionId = firstEventNewBobPov!!.root.content.toModel()!!.sessionId!!
+ val secondSessionId = secondEventNewBobPov!!.root.content.toModel()!!.sessionId!!
+
+ Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
+
+ // Confirm we can decrypt one but not the other
+ testHelper.runBlockingTest {
+ try {
+ newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+ fail("Should not be able to decrypt event")
+ } catch (error: MXCryptoError) {
+ val errorType = (error as? MXCryptoError.Base)?.errorType
+ assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
+ }
+ }
+
+ testHelper.runBlockingTest {
+ try {
+ newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+ } catch (error: MXCryptoError) {
+ fail("Should be able to decrypt event")
+ }
+ }
+
+ // Now let's verify bobs session, and re-request keys
+ bobSessionWithBetterKey.cryptoService()
+ .verificationService()
+ .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+
+ newBobSession.cryptoService()
+ .verificationService()
+ .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
+
+ // now let new session request
+ newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
+
+ // wait a bit
+ testHelper.runBlockingTest {
+ delay(10_000)
+ }
+
+ // old session should have shared the key at earliest known index now
+ // we should be able to decrypt both
+ testHelper.runBlockingTest {
+ try {
+ newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+ } catch (error: MXCryptoError) {
+ fail("Should be able to decrypt first event now $error")
+ }
+ }
+ testHelper.runBlockingTest {
+ try {
+ newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+ } catch (error: MXCryptoError) {
+ fail("Should be able to decrypt event $error")
+ }
+ }
+
+ cryptoTestData.cleanUp(testHelper)
+ testHelper.signOutAndClose(newBobSession)
+ }
+
+ private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
+ aliceRoomPOV.sendTextMessage(text)
+ var sentEventId: String? = null
+ testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
+ val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
+ timeline.start()
+
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val decryptedMsg = timeline.getSnapshot()
+ .filter { it.root.getClearType() == EventType.MESSAGE }
+ .also { list ->
+ val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
+ Log.v("#E2E TEST", "Timeline snapshot is $message")
+ }
+ .filter { it.root.sendState == SendState.SYNCED }
+ .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true }
+ sentEventId = decryptedMsg?.eventId
+ decryptedMsg != null
+ }
+
+ timeline.dispose()
+ }
+ return sentEventId
+ }
+
+ private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) {
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ otherAccounts.map {
+ aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
+ }.all {
+ it == Membership.JOIN
+ }
+ }
+ }
+ }
+
+ private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+ (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+ if (it) {
+ Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
+ }
+ }
+ }
+ }
+
+ testHelper.runBlockingTest(60_000) {
+ Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
+ try {
+ otherSession.joinRoom(e2eRoomID)
+ } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
+ // it's ok we will wait after
+ }
+ }
+
+ Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
+ testHelper.waitWithLatch {
+ testHelper.retryPeriodicallyWithLatch(it) {
+ val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+ roomSummary != null && roomSummary.membership == Membership.JOIN
+ }
+ }
+ }
+
+ private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) {
+ sentEventIds.forEachIndexed { index, sentEventId ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+ testHelper.runBlockingTest {
+ try {
+ session.cryptoService().decryptEvent(event, "").let { result ->
+ event.mxDecryptionResult = OlmDecryptionResult(
+ payload = result.clearEvent,
+ senderKey = result.senderCurve25519Key,
+ keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+ )
+ }
+ } catch (error: MXCryptoError) {
+ // nop
+ }
+ }
+ event.getClearType() == EventType.MESSAGE &&
+ messagesText[index] == event.getClearContent()?.toModel()?.body
+ }
+ }
+ }
+ }
+
+ private fun ensureIsDecrypted(sentEventIds: List, session: Session, e2eRoomID: String) {
+ testHelper.waitWithLatch { latch ->
+ sentEventIds.forEach { sentEventId ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timelineEvent != null &&
+ timelineEvent.isEncrypted() &&
+ timelineEvent.root.getClearType() == EventType.MESSAGE
+ }
+ }
+ }
+ }
+
+ private fun ensureCannotDecrypt(sentEventIds: List, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
+ sentEventIds.forEach { sentEventId ->
+ val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+ testHelper.runBlockingTest {
+ try {
+ newBobSession.cryptoService().decryptEvent(event, "")
+ fail("Should not be able to decrypt event")
+ } catch (error: MXCryptoError) {
+ val errorType = (error as? MXCryptoError.Base)?.errorType
+ if (expectedError == null) {
+ Assert.assertNotNull(errorType)
+ } else {
+ assertEquals(expectedError, errorType, "Message expected to be UISI")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index a7a81bacf5..46c1dacf78 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.FixMethodOrder
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest {
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Test
- @Ignore("This test will be ignored until it is fixed")
fun ensure_outbound_session_happy_path() {
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = testData.roomId
@@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest {
// Just send a real message as test
val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
- assertEquals(megolmSessionId, sentEvent.root.content.toModel()?.sessionId, "Unexpected megolm session")
+ assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId,)
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
index 0a8ce67680..fb5d58b127 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
@@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest {
* -> This is automatically fixed after SDKs restarted the olm session
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testUnwedging() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest {
val bobSession = cryptoTestData.secondSession!!
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
-
- // bobSession.cryptoService().setWarnOnUnknownDevices(false)
- // aliceSession.cryptoService().setWarnOnUnknownDevices(false)
+ val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
@@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest {
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
+ olmDevice.clearOlmSessionCache()
Thread.sleep(6_000)
// Force new session, and key share
@@ -227,8 +224,10 @@ class UnwedgingTest : InstrumentedTest {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
// we should get back the key and be able to decrypt
- val result = tryOrNull {
- bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+ val result = testHelper.runBlockingTest {
+ tryOrNull {
+ bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+ }
}
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index 82aee454eb..cd20ab477c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest {
assert(receivedEvent!!.isEncrypted())
try {
- aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ commonTestHelper.runBlockingTest {
+ aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ }
fail("should fail")
} catch (failure: Throwable) {
}
@@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest {
}
try {
- aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ commonTestHelper.runBlockingTest {
+ aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ }
fail("should fail")
} catch (failure: Throwable) {
}
@@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest {
}
try {
- aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ commonTestHelper.runBlockingTest {
+ aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+ }
} catch (failure: Throwable) {
fail("should have been able to decrypt")
}
@@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest {
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
- var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
+ var dRes = tryOrNull {
+ commonTestHelper.runBlockingTest {
+ bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
+ }
+ }
assert(dRes == null)
@@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(3_000)
// With the bug the first session would have improperly reshare that key :/
- dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
+ dRes = tryOrNull {
+ commonTestHelper.runBlockingTest {
+ bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
+ }
+ }
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}")
assert(dRes?.clearEvent == null)
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index 9fda21763a..65c65660b5 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest {
// Bob should not be able to decrypt because the keys is withheld
try {
// .. might need to wait a bit for stability?
- bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+ testHelper.runBlockingTest {
+ bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+ }
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
@@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest {
// Previous message should still be undecryptable (partially withheld session)
try {
// .. might need to wait a bit for stability?
- bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+ testHelper.runBlockingTest {
+ bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+ }
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
@@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest {
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
try {
// .. might need to wait a bit for stability?
- bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+ testHelper.runBlockingTest {
+ bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+ }
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
@@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest {
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
// try to decrypt and force key request
- tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
+ tryOrNull {
+ testHelper.runBlockingTest {
+ bobSecondSession.cryptoService().decryptEvent(it.root, "")
+ }
+ }
}
sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId
timeLineEvent != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index 35c5a4dab9..2c96568102 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -39,6 +40,7 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
+@Ignore("This test is flaky ; see issue #5449")
class VerificationTest : InstrumentedTest {
data class ExpectedResult(
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
index 9856ee7770..1e3512a9df 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
@@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
)
- ))
+ ),
+ TestPermalinkService()
+ )
)
@Test
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
new file mode 100644
index 0000000000..2f9a5e0a73
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 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.room.send
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
+
+class TestPermalinkService : PermalinkService {
+ override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
+ return null
+ }
+
+ override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
+ return ""
+ }
+
+ override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
+ return ""
+ }
+
+ override fun createRoomPermalink(roomId: String, viaServers: List?, forceMatrixTo: Boolean): String? {
+ return null
+ }
+
+ override fun getLinkedId(url: String): String? {
+ return null
+ }
+
+ override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+ return when (type) {
+ HTML -> "%2\$s"
+ MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index 6aa4f4cc32..dcb181f0c1 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -22,6 +22,7 @@ import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -38,6 +39,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
+@Ignore("Remaining Integration tests are unstable if run with this test. Issue #5439")
class ThreadMessagingTest : InstrumentedTest {
@Test
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
index 69ae57e644..5c011c8b2f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
@@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealm(it)
}
- chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+ chunk.addTimelineEvent(
+ roomId = ROOM_ID,
+ eventEntity = fakeEvent,
+ direction = PaginationDirection.FORWARDS,
+ roomMemberContentsByUser = emptyMap())
chunk.timelineEvents.size shouldBeEqualTo 1
}
}
@@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealm(it)
}
- chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
- chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+ chunk.addTimelineEvent(
+ roomId = ROOM_ID,
+ eventEntity = fakeEvent,
+ direction = PaginationDirection.FORWARDS,
+ roomMemberContentsByUser = emptyMap())
+ chunk.addTimelineEvent(
+ roomId = ROOM_ID,
+ eventEntity = fakeEvent,
+ direction = PaginationDirection.FORWARDS,
+ roomMemberContentsByUser = emptyMap())
chunk.timelineEvents.size shouldBeEqualTo 1
}
}
@@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealm(it)
}
- addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
+ addTimelineEvent(
+ roomId = roomId,
+ eventEntity = fakeEvent,
+ direction = direction,
+ roomMemberContentsByUser = emptyMap())
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
new file mode 100644
index 0000000000..6792d6ddfd
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.session.room.timeline
+
+import org.amshove.kluent.fail
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeGreaterThan
+import org.amshove.kluent.shouldContain
+import org.amshove.kluent.shouldContainAll
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
+import org.matrix.android.sdk.api.session.room.model.message.PollType
+import org.matrix.android.sdk.api.session.room.timeline.Timeline
+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.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class PollAggregationTest : InstrumentedTest {
+
+ @Test
+ fun testAllPollUseCases() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomId = cryptoTestData.roomId
+ val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
+
+ val roomFromBobPOV = cryptoTestData.secondSession!!.getRoom(cryptoTestData.roomId)!!
+ // Bob creates a poll
+ roomFromBobPOV.sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
+
+ aliceSession.startSync(true)
+ val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
+ aliceTimeline.start()
+
+ val TOTAL_TEST_COUNT = 7
+ val lock = CountDownLatch(TOTAL_TEST_COUNT)
+
+ val aliceEventsListener = object : Timeline.Listener {
+ override fun onTimelineUpdated(snapshot: List) {
+ snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+ val pollEventId = pollEvent.eventId
+ val pollContent = pollEvent.root.content?.toModel()
+ val pollSummary = pollEvent.annotations?.pollResponseSummary
+
+ if (pollContent == null) {
+ fail("Poll content is null")
+ return
+ }
+
+ when (lock.count.toInt()) {
+ TOTAL_TEST_COUNT -> {
+ // Poll has just been created.
+ testInitialPollConditions(pollContent, pollSummary)
+ lock.countDown()
+ roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+ }
+ TOTAL_TEST_COUNT - 1 -> {
+ // Bob: Option 1
+ testBobVotesOption1(pollContent, pollSummary)
+ lock.countDown()
+ roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+ }
+ TOTAL_TEST_COUNT - 2 -> {
+ // Bob: Option 2
+ testBobChangesVoteToOption2(pollContent, pollSummary)
+ lock.countDown()
+ roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+ }
+ TOTAL_TEST_COUNT - 3 -> {
+ // Alice: Option 2, Bob: Option 2
+ testAliceAndBobVoteToOption2(pollContent, pollSummary)
+ lock.countDown()
+ roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+ }
+ TOTAL_TEST_COUNT - 4 -> {
+ // Alice: Option 1, Bob: Option 2
+ testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+ lock.countDown()
+ roomFromBobPOV.endPoll(pollEventId)
+ }
+ TOTAL_TEST_COUNT - 5 -> {
+ // Alice: Option 1, Bob: Option 2 [poll is ended]
+ testEndedPoll(pollSummary)
+ lock.countDown()
+ roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+ }
+ TOTAL_TEST_COUNT - 6 -> {
+ // Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
+ testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+ testEndedPoll(pollSummary)
+ lock.countDown()
+ }
+ else -> {
+ fail("Lock count ${lock.count} didn't handled.")
+ }
+ }
+ }
+ }
+ }
+
+ aliceTimeline.addListener(aliceEventsListener)
+
+ commonTestHelper.await(lock)
+
+ aliceTimeline.removeAllListeners()
+
+ aliceSession.stopSync()
+ aliceTimeline.dispose()
+ cryptoTestData.cleanUp(commonTestHelper)
+ }
+
+ private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+ // No votes yet, poll summary should be null
+ pollSummary shouldBe null
+ // Question should be the same as intended
+ pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
+ // Options should be the same as intended
+ pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
+ answers.size shouldBeEqualTo pollOptions.size
+ answers.map { it.getBestAnswer() } shouldContainAll pollOptions
+ }
+ }
+
+ private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+ if (pollSummary == null) {
+ fail("Poll summary shouldn't be null when someone votes")
+ return
+ }
+ val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
+ // Check if the intended vote is in poll summary
+ pollSummary.aggregatedContent?.let { aggregatedContent ->
+ assertTotalVotesCount(aggregatedContent, 1)
+ aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+ aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+ aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+ } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+ }
+
+ private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+ if (pollSummary == null) {
+ fail("Poll summary shouldn't be null when someone votes")
+ return
+ }
+ val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+ // Check if the intended vote is in poll summary
+ pollSummary.aggregatedContent?.let { aggregatedContent ->
+ assertTotalVotesCount(aggregatedContent, 1)
+ aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+ aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+ aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+ } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+ }
+
+ private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+ if (pollSummary == null) {
+ fail("Poll summary shouldn't be null when someone votes")
+ return
+ }
+ val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+ // Check if the intended votes is in poll summary
+ pollSummary.aggregatedContent?.let { aggregatedContent ->
+ assertTotalVotesCount(aggregatedContent, 2)
+ aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+ aggregatedContent.votes?.get(1)?.option shouldBeEqualTo answerId
+ aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 2
+ aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+ } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+ }
+
+ private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+ if (pollSummary == null) {
+ fail("Poll summary shouldn't be null when someone votes")
+ return
+ }
+ val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
+ val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+ // Check if the intended votes is in poll summary
+ pollSummary.aggregatedContent?.let { aggregatedContent ->
+ assertTotalVotesCount(aggregatedContent, 2)
+ aggregatedContent.votes!!.map { it.option } shouldContain firstAnswerId
+ aggregatedContent.votes!!.map { it.option } shouldContain secondAnswerId
+ aggregatedContent.votesSummary?.get(firstAnswerId)?.total shouldBeEqualTo 1
+ aggregatedContent.votesSummary?.get(secondAnswerId)?.total shouldBeEqualTo 1
+ aggregatedContent.votesSummary?.get(firstAnswerId)?.percentage shouldBeEqualTo 0.5
+ aggregatedContent.votesSummary?.get(secondAnswerId)?.percentage shouldBeEqualTo 0.5
+ } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+ }
+
+ private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
+ pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
+ }
+
+ private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
+ aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
+ aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
+ }
+
+ companion object {
+ const val pollQuestion = "Do you like creating polls?"
+ val pollOptions = listOf("Yes", "Absolutely", "As long as tests pass")
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index c87f21d7ac..c4bc289b75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -60,7 +60,15 @@ data class MatrixConfiguration(
/**
* RoomDisplayNameFallbackProvider to provide default room display name.
*/
- val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
+ val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
+ /**
+ * True to enable presence information sync (if available). False to disable regardless of server setting.
+ */
+ val presenceSyncEnabled: Boolean = true,
+ /**
+ * Thread messages default enable/disabled value
+ */
+ val threadMessagesEnabledDefault: Boolean = false,
) {
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index aabe6e0d06..89b4a343dd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -58,12 +58,36 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long {
?: defaultValue
}
+fun Throwable.isUsernameInUse(): Boolean {
+ return this is Failure.ServerError && error.code == MatrixError.M_USER_IN_USE
+}
+
+fun Throwable.isInvalidUsername(): Boolean {
+ return this is Failure.ServerError &&
+ error.code == MatrixError.M_INVALID_USERNAME
+}
+
fun Throwable.isInvalidPassword(): Boolean {
return this is Failure.ServerError &&
error.code == MatrixError.M_FORBIDDEN &&
error.message == "Invalid password"
}
+fun Throwable.isRegistrationDisabled(): Boolean {
+ return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN &&
+ httpCode == HttpsURLConnection.HTTP_FORBIDDEN
+}
+
+fun Throwable.isWeakPassword(): Boolean {
+ return this is Failure.ServerError && error.code == MatrixError.M_WEAK_PASSWORD
+}
+
+fun Throwable.isLoginEmailUnknown(): Boolean {
+ return this is Failure.ServerError &&
+ error.code == MatrixError.M_FORBIDDEN &&
+ error.message.isEmpty()
+}
+
fun Throwable.isInvalidUIAAuth(): Boolean {
return this is Failure.ServerError &&
error.code == MatrixError.M_FORBIDDEN &&
@@ -104,8 +128,8 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
return this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
(error.code == MatrixError.M_USER_IN_USE ||
- error.code == MatrixError.M_INVALID_USERNAME ||
- error.code == MatrixError.M_EXCLUSIVE)
+ error.code == MatrixError.M_INVALID_USERNAME ||
+ error.code == MatrixError.M_EXCLUSIVE)
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index e3f00a24b6..65f69e17c9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -121,7 +121,7 @@ interface CryptoService {
fun discardOutboundSession(roomId: String)
@Throws(MXCryptoError::class)
- fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+ suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index 34096d603f..ae8ed3941f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AggregatedRelations(
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
- @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
+ @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+ @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index df57ca5681..f1304f6216 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -201,7 +201,11 @@ data class Event(
*/
fun getDecryptedTextSummary(): String? {
if (isRedacted()) return "Message Deleted"
- val text = getDecryptedValue() ?: return null
+ val text = getDecryptedValue() ?: run {
+ if (isPoll()) { return getPollQuestion() ?: "created a poll." }
+ return null
+ }
+
return when {
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
isFileMessage() -> "sent a file."
@@ -349,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean {
}
}
-fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
+fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
@@ -372,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
* Returns the poll question or null otherwise
*/
fun Event.getPollQuestion(): String? =
- getPollContent()?.pollCreationInfo?.question?.question
+ getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
/**
* Returns the relation content for a specific type or null otherwise
@@ -385,12 +389,12 @@ fun Event.isReply(): Boolean {
}
fun Event.isReplyRenderedInThread(): Boolean {
- return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
+ return isReply() && getRelationContent()?.shouldRenderInThread() == true
}
-fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
-fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
fun Event.isEdition(): Boolean {
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
@@ -406,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
fun Event.getPollContent(): MessagePollContent? {
return content.toModel()
}
+
+fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 0c77b574e7..22fb9bcbe2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -103,9 +103,9 @@ object EventType {
const val REACTION = "m.reaction"
// Poll
- const val POLL_START = "org.matrix.msc3381.poll.start"
- const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
- const val POLL_END = "org.matrix.msc3381.poll.end"
+ val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
+ val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
+ val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
// Unwedging
internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
new file mode 100644
index 0000000000..cc52dfc02c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class LatestThreadUnsignedRelation(
+ override val limited: Boolean? = false,
+ override val count: Int? = 0,
+ @Json(name = "latest_event")
+ val event: Event? = null,
+ @Json(name = "current_user_participated")
+ val isUserParticipating: Boolean? = false
+
+) : UnsignedRelationInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
index fb26264ad7..74dc74b294 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
@@ -30,7 +30,6 @@ object RelationType {
/** Lets you define an event which is a thread reply to an existing event.*/
const val THREAD = "m.thread"
- const val IO_THREAD = "io.element.thread"
/** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "org.matrix.response"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 2256dfb8f0..9db3876b74 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -50,7 +50,11 @@ data class HomeServerCapabilities(
* This capability describes the default and available room versions a server supports, and at what level of stability.
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
*/
- val roomVersions: RoomVersionCapabilities? = null
+ val roomVersions: RoomVersionCapabilities? = null,
+ /**
+ * True if the home server support threading
+ */
+ var canUseThreading: Boolean = false
) {
enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
index 920dc85c7a..c139da813a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
@@ -28,6 +28,11 @@ interface PermalinkService {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
}
+ enum class SpanTemplateType {
+ HTML,
+ MARKDOWN
+ }
+
/**
* Creates a permalink for an event.
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
@@ -80,4 +85,15 @@ interface PermalinkService {
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
*/
fun getLinkedId(url: String): String?
+
+ /**
+ * Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
+ * Ex: "%2\$s" or "[%2\$s](https://matrix.to/#/%1\$s)"
+ *
+ * @param type: type of template to create
+ * @param forceMatrixTo whether we should force using matrix.to base URL
+ *
+ * @return the created template
+ */
+ fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
index 05fa24946a..d2c677bb31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
@@ -21,6 +21,7 @@ import android.net.Uri
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
@@ -118,4 +119,17 @@ interface ProfileService {
* Remove a 3Pid from the Matrix account.
*/
suspend fun deleteThreePid(threePid: ThreePid)
+
+ /**
+ * Return a User object from a userId
+ */
+ suspend fun getProfileAsUser(userId: String): User {
+ return getProfile(userId).let { dict ->
+ User(
+ userId = userId,
+ displayName = dict[DISPLAY_NAME_KEY] as? String,
+ avatarUrl = dict[AVATAR_URL_KEY] as? String
+ )
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index d930a5d0fd..be65b883b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
interface Room :
TimelineService,
ThreadsService,
+ ThreadsLocalService,
SendService,
DraftService,
ReadService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index bca432320d..aec358218b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -216,6 +216,12 @@ interface RoomService {
pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
+ /**
+ * Return a LiveData on the number of rooms
+ * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
+ */
+ fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData
+
/**
* TODO Doc
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
index b83f57f5ef..db87f913b9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
@@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
interface UpdatableLivePageResult {
val livePagedList: LiveData>
-
- fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
-
val liveBoundaries: LiveData
+ var queryParams: RoomSummaryQueryParams
}
data class ResultBoundaries(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
index e8b3cf2488..35fa555a5b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
@@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LocationAsset(
- @Json(name = "type") val type: LocationAssetType? = null
+ @Json(name = "type") val type: String? = null
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
index ef40e21c47..f7d82d4b40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
@@ -16,11 +16,20 @@
package org.matrix.android.sdk.api.session.room.model.message
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
+/**
+ * Define what particular asset is being referred to.
+ * We don't use enum type since it is not limited to a specific set of values.
+ * The way this type should be interpreted in client side is described in
+ * [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+ */
+object LocationAssetType {
+ /**
+ * Used for user location sharing.
+ **/
+ const val SELF = "m.self"
-@JsonClass(generateAdapter = false)
-enum class LocationAssetType {
- @Json(name = "m.self")
- SELF
+ /**
+ * Used for pin drop location sharing.
+ **/
+ const val PIN = "m.pin"
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index d07bd2d73a..2052133b06 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -39,37 +39,47 @@ data class MessageLocationContent(
*/
@Json(name = "geo_uri") val geoUri: String,
- /**
- * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
- */
- @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
-
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
-
/**
- * m.asset defines a generic asset that can be used for location tracking but also in other places like
- * inventories, geofencing, checkins/checkouts etc.
- * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
- * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
+ * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
*/
- @Json(name = "m.asset") val locationAsset: LocationAsset? = null,
-
+ @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
+ @Json(name = "m.location") val locationInfo: LocationInfo? = null,
/**
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
*/
- @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
-
- @Json(name = "org.matrix.msc1767.text") val text: String? = null
+ @Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
+ @Json(name = "m.ts") val ts: Long? = null,
+ @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
+ @Json(name = "m.text") val text: String? = null,
+ /**
+ * Defines a generic asset that can be used for location tracking but also in other places like
+ * inventories, geofencing, checkins/checkouts etc.
+ * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
+ * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
+ * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+ */
+ @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
+ @Json(name = "m.asset") val locationAsset: LocationAsset? = null
) : MessageContent {
- fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
+ fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
+
+ fun getBestTs() = ts ?: unstableTs
+
+ fun getBestText() = text ?: unstableText
+
+ fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
+
+ fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
/**
* @return true if the location asset is a user location, not a generic one.
*/
fun isSelfLocation(): Boolean {
// Should behave like m.self if locationAsset is null
+ val locationAsset = getBestLocationAsset()
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
index a4e1317290..43c0c90068 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -31,5 +31,9 @@ data class MessagePollContent(
@Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
- @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
-) : MessageContent
+ @Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
+ @Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
+) : MessageContent {
+
+ fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
index f3b4e3dc23..022915ed69 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
@@ -31,5 +31,9 @@ data class MessagePollResponseContent(
@Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
- @Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
-) : MessageContent
+ @Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
+ @Json(name = "m.response") val response: PollResponse? = null
+) : MessageContent {
+
+ fun getBestResponse() = response ?: unstableResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
index 8f5ff53c85..34614d9d15 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PollAnswer(
@Json(name = "id") val id: String? = null,
- @Json(name = "org.matrix.msc1767.text") val answer: String? = null
-)
+ @Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
+ @Json(name = "m.text") val answer: String? = null
+) {
+
+ fun getBestAnswer() = answer ?: unstableAnswer
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
index a82c01b159..81b034a809 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PollCreationInfo(
- @Json(name = "question") val question: PollQuestion? = null,
- @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
- @Json(name = "max_selections") val maxSelections: Int = 1,
- @Json(name = "answers") val answers: List? = null
+ @Json(name = "question") val question: PollQuestion? = null,
+ @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
+ @Json(name = "max_selections") val maxSelections: Int = 1,
+ @Json(name = "answers") val answers: List? = null
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
index 76025f745e..df9517892b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PollQuestion(
- @Json(name = "org.matrix.msc1767.text") val question: String? = null
-)
+ @Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
+ @Json(name = "m.text") val question: String? = null
+) {
+
+ fun getBestQuestion() = question ?: unstableQuestion
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
index 3a8066b9bc..54801e698d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
@@ -25,11 +25,17 @@ enum class PollType {
* Voters should see results as soon as they have voted.
*/
@Json(name = "org.matrix.msc3381.poll.disclosed")
+ DISCLOSED_UNSTABLE,
+
+ @Json(name = "m.poll.disclosed")
DISCLOSED,
/**
* Results should be only revealed when the poll is ended.
*/
@Json(name = "org.matrix.msc3381.poll.undisclosed")
+ UNDISCLOSED_UNSTABLE,
+
+ @Json(name = "m.poll.undisclosed")
UNDISCLOSED
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
index 733d6c37e8..e7bebeeff6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
@@ -26,5 +26,6 @@ data class ReactionInfo(
@Json(name = "key") val key: String,
// always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
- @Json(name = "option") override val option: Int? = null
+ @Json(name = "option") override val option: Int? = null,
+ @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
) : RelationContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
index e2080bb437..53b1fea873 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
@@ -24,4 +24,10 @@ interface RelationContent {
val eventId: String?
val inReplyTo: ReplyToContent?
val option: Int?
+
+ /**
+ * This flag indicates that the message should be rendered as a reply
+ * fallback, when isFallingBack = false
+ */
+ val isFallingBack: Boolean?
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
index 10b071a601..5dcb1b4323 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
@@ -23,5 +23,8 @@ data class RelationDefaultContent(
@Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String?,
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
- @Json(name = "option") override val option: Int? = null
+ @Json(name = "option") override val option: Int? = null,
+ @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
) : RelationContent
+
+fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 09114436f0..4409898908 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
@@ -163,13 +163,4 @@ interface RelationService {
autoMarkdown: Boolean = false,
formattedText: String? = null,
eventReplied: TimelineEvent? = null): Cancelable?
-
- /**
- * Get all the thread replies for the specified rootThreadEventId
- * The return list will contain the original root thread event and all the thread replies to that event
- * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
- * from the backend
- * @param rootThreadEventId the root thread eventId
- */
- suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
index 412a1bfca9..251328bea2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
@@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReplyToContent(
- @Json(name = "event_id") val eventId: String? = null,
- @Json(name = "render_in") val renderIn: List? = null
+ @Json(name = "event_id") val eventId: String? = null
)
-
-fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 913dbfd010..9f8b1d93d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -142,8 +142,9 @@ interface SendService {
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
+ * @param isUserLocation indicates whether the location data corresponds to the user location or not
*/
- fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
+ fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
/**
* Remove this failed message from the timeline
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 3bba2deae5..eaed9053ea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -32,7 +32,6 @@ object RoomSummaryConstants {
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STICKER,
- EventType.REACTION,
- EventType.POLL_START
- )
+ EventType.REACTION
+ ) + EventType.POLL_START
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index e4d1d979e1..839cdff63b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -17,51 +17,43 @@
package org.matrix.android.sdk.api.session.room.threads
import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
/**
- * This interface defines methods to interact with threads related features.
- * It's implemented at the room level within the main timeline.
+ * This interface defines methods to interact with thread related features.
+ * It's the dynamic threads implementation and the homeserver must return
+ * a capability entry for threads. If the server do not support m.thread
+ * then [ThreadsLocalService] should be used instead
*/
interface ThreadsService {
/**
- * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+ * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
*/
- fun getAllThreadsLive(): LiveData>
+ fun getAllThreadSummariesLive(): LiveData>
/**
- * Returns a list of all the thread root TimelineEvents that exists at the room level
+ * Returns a list of all the [ThreadSummary] that exists at the room level
*/
- fun getAllThreads(): List
+ fun getAllThreadSummaries(): List
/**
- * Returns a [LiveData] list of all the marked unread threads that exists at the room level
- */
- fun getMarkedThreadNotificationsLive(): LiveData>
-
- /**
- * Returns a list of all the marked unread threads that exists at the room level
- */
- fun getMarkedThreadNotifications(): List
-
- /**
- * Returns whether or not the current user is participating in the thread
- * @param rootThreadEventId the eventId of the current thread
- */
- fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
-
- /**
- * Enhance the provided root thread TimelineEvent [List] by adding the latest
+ * Enhance the provided ThreadSummary[List] by adding the latest
* message edition for that thread
* @return the enhanced [List] with edited updates
*/
- fun mapEventsWithEdition(threads: List): List
+ fun enhanceThreadWithEditions(threads: List): List
/**
- * Marks the current thread as read in local DB.
- * note: read receipts within threads are not yet supported with the API
- * @param rootThreadEventId the root eventId of the current thread
+ * Fetch all thread replies for the specified thread using the /relations api
+ * @param rootThreadEventId the root thread eventId
+ * @param from defines the token that will fetch from that position
+ * @param limit defines the number of max results the api will respond with
*/
- suspend fun markThreadAsRead(rootThreadEventId: String)
+ suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
+
+ /**
+ * Fetch all thread summaries for the current room using the enhanced /messages api
+ */
+ suspend fun fetchThreadSummaries()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
new file mode 100644
index 0000000000..f7b379e382
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.threads.local
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This interface defines methods to interact with thread related features.
+ * It's the local threads implementation and assumes that the homeserver
+ * do not support threads
+ */
+interface ThreadsLocalService {
+
+ /**
+ * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+ */
+ fun getAllThreadsLive(): LiveData>
+
+ /**
+ * Returns a list of all the thread root TimelineEvents that exists at the room level
+ */
+ fun getAllThreads(): List
+
+ /**
+ * Returns a [LiveData] list of all the marked unread threads that exists at the room level
+ */
+ fun getMarkedThreadNotificationsLive(): LiveData>
+
+ /**
+ * Returns a list of all the marked unread threads that exists at the room level
+ */
+ fun getMarkedThreadNotifications(): List
+
+ /**
+ * Returns whether or not the current user is participating in the thread
+ * @param rootThreadEventId the eventId of the current thread
+ */
+ fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+
+ /**
+ * Enhance the provided root thread TimelineEvent [List] by adding the latest
+ * message edition for that thread
+ * @return the enhanced [List] with edited updates
+ */
+ fun mapEventsWithEdition(threads: List): List
+
+ /**
+ * Marks the current thread as read in local DB.
+ * note: read receipts within threads are not yet supported with the API
+ * @param rootThreadEventId the root eventId of the current thread
+ */
+ suspend fun markThreadAsRead(rootThreadEventId: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
new file mode 100644
index 0000000000..c8353cf0de
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.threads.model
+
+data class ThreadEditions(var rootThreadEdition: String? = null,
+ var latestThreadEdition: String? = null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
new file mode 100644
index 0000000000..017afba1ba
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.threads.model
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+/**
+ * The main thread Summary model, mainly used to display the thread list
+ */
+data class ThreadSummary(val roomId: String,
+ val rootEvent: Event?,
+ val latestEvent: Event?,
+ val rootEventId: String,
+ val rootThreadSenderInfo: SenderInfo,
+ val latestThreadSenderInfo: SenderInfo,
+ val isUserParticipating: Boolean,
+ val numberOfThreads: Int,
+ val threadEditions: ThreadEditions = ThreadEditions())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
new file mode 100644
index 0000000000..95697f987f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.threads.model
+
+enum class ThreadSummaryUpdateType {
+ REPLACE,
+ ADD
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 6f8bae876b..1b01efc074 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.room.timeline
import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.orFalse
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.RelationType
@@ -54,6 +55,7 @@ data class TimelineEvent(
* It's not unique on the timeline as it's reset on each chunk.
*/
val displayIndex: Int,
+ var ownedByThreadChunk: Boolean = false,
val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List = emptyList()
@@ -134,9 +136,9 @@ fun TimelineEvent.getEditedEventId(): String? {
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? {
return when (root.getClearType()) {
- EventType.STICKER -> root.getClearContent().toModel()
- EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
- else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+ EventType.STICKER -> root.getClearContent().toModel()
+ in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+ else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
}
}
@@ -158,6 +160,13 @@ fun TimelineEvent.isSticker(): Boolean {
return root.isSticker()
}
+/**
+ * Returns whether or not the event is a root thread event
+ */
+fun TimelineEvent.isRootThread(): Boolean {
+ return root.threadDetails?.isRootThread.orFalse()
+}
+
/**
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 6152069644..46433f387d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -38,7 +38,7 @@ interface TimelineService {
/**
* Returns a snapshot of TimelineEvent event with eventId.
- * At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
+ * At the opposite of getTimelineEventLive which will be updated when local echo event is synced, it will return null in this case.
* @param eventId the eventId to get the TimelineEvent
*/
fun getTimelineEvent(eventId: String): TimelineEvent?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
index fafe17b2c0..d6937d5b26 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.api.session.threads
+import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
/**
@@ -26,7 +27,7 @@ data class ThreadDetails(
val isRootThread: Boolean = false,
val numberOfThreads: Int = 0,
val threadSummarySenderInfo: SenderInfo? = null,
- val threadSummaryLatestTextMessage: String? = null,
+ val threadSummaryLatestEvent: Event? = null,
val lastMessageTimestamp: Long? = null,
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
val isThread: Boolean = false,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 302f7387fa..650b8cc26d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -199,6 +200,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
+fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
+
fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
} else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index 0a9b8b73cc..815f8de2de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -38,7 +38,7 @@ internal data class HomeServerVersion(
}
companion object {
- internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
+ internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
internal fun parse(value: String): HomeServerVersion? {
val result = pattern.matchEntire(value) ?: return null
@@ -56,5 +56,6 @@ internal data class HomeServerVersion(
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+ val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 74cb3de2ac..d07d5ecd64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -51,6 +51,8 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
+private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
+private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
/**
* Return true if the SDK supports this homeserver version
@@ -68,6 +70,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
doesServerSeparatesAddAndBind()
}
+/**
+ * Indicate if the homeserver support MSC3440 for threads
+ */
+internal fun Versions.doesServerSupportThreads(): Boolean {
+ return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
+ unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
+}
+
/**
* Return true if the server support the lazy loading of room members
*
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 0646e4d2b8..db44abc36f 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -434,6 +434,14 @@ internal class DefaultCryptoService @Inject constructor(
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
+
+ // unwedge if needed
+ try {
+ eventDecryptor.unwedgeDevicesIfNeeded()
+ } catch (failure: Throwable) {
+ Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
+ }
+
// There is a limit of to_device events returned per sync.
// If we are in a case of such limited to_device sync we can't try to generate/upload
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
@@ -723,7 +731,7 @@ internal class DefaultCryptoService @Inject constructor(
* @return the MXEventDecryptionResult data, or throw in case of error
*/
@Throws(MXCryptoError::class)
- override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
}
@@ -746,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor(
* @return the MXEventDecryptionResult data, or null in case of error
*/
@Throws(MXCryptoError::class)
- private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return eventDecryptor.decryptEvent(event, timeline)
}
@@ -1364,6 +1372,9 @@ internal class DefaultCryptoService @Inject constructor(
@VisibleForTesting
val cryptoStoreForTesting = cryptoStore
+ @VisibleForTesting
+ val olmDeviceForTest = olmDevice
+
companion object {
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index 57381eacfb..00efd3d6a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -21,14 +21,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
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.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-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.event.OlmEventContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -40,6 +39,8 @@ import javax.inject.Inject
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
+private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
+
@SessionScope
internal class EventDecryptor @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
@@ -47,13 +48,22 @@ internal class EventDecryptor @Inject constructor(
private val roomDecryptorProvider: RoomDecryptorProvider,
private val messageEncrypter: MessageEncrypter,
private val sendToDeviceTask: SendToDeviceTask,
+ private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore
) {
- // The date of the last time we forced establishment
- // of a new session for each user:device.
- private val lastNewSessionForcedDates = MXUsersDevicesMap()
+ /**
+ * Rate limit unwedge attempt, should we persist that?
+ */
+ private val lastNewSessionForcedDates = mutableMapOf()
+
+ data class WedgedDeviceInfo(
+ val userId: String,
+ val senderKey: String?
+ )
+
+ private val wedgedDevices = mutableListOf()
/**
* Decrypt an event
@@ -63,7 +73,7 @@ internal class EventDecryptor @Inject constructor(
* @return the MXEventDecryptionResult data, or throw in case of error
*/
@Throws(MXCryptoError::class)
- fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
}
@@ -91,38 +101,32 @@ internal class EventDecryptor @Inject constructor(
* @return the MXEventDecryptionResult data, or null in case of error
*/
@Throws(MXCryptoError::class)
- private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content
if (eventContent == null) {
- Timber.e("## CRYPTO | decryptEvent : empty event content")
+ Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
- Timber.e("## CRYPTO | decryptEvent() : $reason")
+ Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else {
try {
return alg.decryptEvent(event, timeline)
} catch (mxCryptoError: MXCryptoError) {
- Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+ Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base &&
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- val olmContent = event.content.toModel()
- cryptoStore.getUserDevices(event.senderId ?: "")
- ?.values
- ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
- ?.let {
- markOlmSessionForUnwedging(event.senderId ?: "", it)
- }
- ?: run {
- Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
- }
+ val olmContent = event.content.toModel()
+ if (event.senderId != null && olmContent?.senderKey != null) {
+ markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
+ } else {
+ Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
}
}
}
@@ -132,53 +136,91 @@ internal class EventDecryptor @Inject constructor(
}
}
- // coroutineDispatchers.crypto scope
- private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
- val deviceKey = deviceInfo.identityKey()
+ private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
+ val info = WedgedDeviceInfo(senderId, senderKey)
+ if (!wedgedDevices.contains(info)) {
+ Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
+ wedgedDevices.add(info)
+ }
+ }
- val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+ // coroutineDispatchers.crypto scope
+ suspend fun unwedgeDevicesIfNeeded() {
+ // handle wedged devices
+ // Some olm decryption have failed and some device are wedged
+ // we should force start a new session for those
+ Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
+ // get the one that should be retried according to rate limit
val now = System.currentTimeMillis()
- if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
- Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+ val toUnwedge = wedgedDevices.filter {
+ val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
+ if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+ Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
+ return@filter false
+ }
+ // let's already mark that we tried now
+ lastNewSessionForcedDates[it] = now
+ true
+ }
+
+ if (toUnwedge.isEmpty()) {
+ Timber.tag(loggerTag.value).v("Nothing to unwedge")
return
}
+ Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
- Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
- lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
-
- // offload this from crypto thread (?)
- cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
- runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
- onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
- onFailure = {
- Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
+ toUnwedge
+ .chunked(100) // safer to chunk if we ever have lots of wedged devices
+ .forEach { wedgedList ->
+ val groupedByUserId = wedgedList.groupBy { it.userId }
+ // lets download keys if needed
+ withContext(coroutineDispatchers.io) {
+ deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
}
- )
- }
- }
- private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap, deviceInfo: CryptoDeviceInfo, senderId: String) {
- Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
+ // find the matching devices
+ groupedByUserId
+ .map { groupedByUser ->
+ val userId = groupedByUser.key
+ val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
+ val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
+ userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
+ knownDevices.firstOrNull { it.identityKey() == senderKey }
+ }
+ }
+ .toMap()
+ .let { deviceList ->
+ try {
+ // force creating new outbound session and mark them as most recent to
+ // be used for next encryption (dummy)
+ val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
+ Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
- // Now send a blank message on that session so the other side knows about it.
- // (The keyshare request is sent in the clear so that won't do)
- // We send this first such that, as long as the toDevice messages arrive in the
- // same order we sent them, the other end will get this first, set up the new session,
- // then get the keyshare request and send the key over this new session (because it
- // is the session it has most recently received a message on).
- val payloadJson = mapOf("type" to EventType.DUMMY)
+ // Now send a dummy message on that session so the other side knows about it.
+ val payloadJson = mapOf(
+ "type" to EventType.DUMMY
+ )
+ val sendToDeviceMap = MXUsersDevicesMap()
+ sessionToUse.map.values
+ .flatMap { it.values }
+ .map { it.deviceInfo }
+ .forEach { deviceInfo ->
+ Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
+ val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+ sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
+ }
- val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
- val sendToDeviceMap = MXUsersDevicesMap()
- sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
- Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
- withContext(coroutineDispatchers.io) {
- val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
- try {
- sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
- } catch (failure: Throwable) {
- Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
- }
- }
+ // now let's send that
+ val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+ withContext(coroutineDispatchers.io) {
+ sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
+ }
+ } catch (failure: Throwable) {
+ deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
+ Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
+ }
+ }
+ }
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index e7a46750b0..34bef61c98 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto
import android.util.LruCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
@@ -28,6 +30,13 @@ import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject
+data class InboundGroupSessionHolder(
+ val wrapper: OlmInboundGroupSessionWrapper2,
+ val mutex: Mutex = Mutex()
+)
+
+private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
+
/**
* Allows to cache and batch store operations on inbound group session store.
* Because it is used in the decrypt flow, that can be called quite rapidly
@@ -42,12 +51,13 @@ internal class InboundGroupSessionStore @Inject constructor(
val senderKey: String
)
- private val sessionCache = object : LruCache(30) {
- override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
- if (evicted && oldValue != null) {
+ private val sessionCache = object : LruCache(100) {
+ override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
+ if (oldValue != null) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
- store.storeInboundGroupSessions(listOf(oldValue))
+ Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
+ store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
+ oldValue.wrapper.olmInboundGroupSession?.releaseSession()
}
}
}
@@ -59,27 +69,50 @@ internal class InboundGroupSessionStore @Inject constructor(
private val dirtySession = mutableListOf()
@Synchronized
- fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
- synchronized(sessionCache) {
- val known = sessionCache[CacheKey(sessionId, senderKey)]
- Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
- return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
- Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
- sessionCache.put(CacheKey(sessionId, senderKey), it)
- }
- }
+ fun clear() {
+ sessionCache.evictAll()
}
@Synchronized
- fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) {
- Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+ fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
+ val known = sessionCache[CacheKey(sessionId, senderKey)]
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}")
+ return known
+ ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+ sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
+ }?.let {
+ InboundGroupSessionHolder(it)
+ }
+ }
+
+ @Synchronized
+ fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
+ dirtySession.remove(old.wrapper)
+ store.removeInboundGroupSession(sessionId, senderKey)
+ sessionCache.remove(CacheKey(sessionId, senderKey))
+
+ // release removed session
+ old.wrapper.olmInboundGroupSession?.releaseSession()
+
+ internalStoreGroupSession(new, sessionId, senderKey)
+ }
+
+ @Synchronized
+ fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ internalStoreGroupSession(holder, sessionId, senderKey)
+ }
+
+ private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
// We want to batch this a bit for performances
- dirtySession.add(wrapper)
+ dirtySession.add(holder.wrapper)
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert
// If it's already known, no need to update cache it's already there
- sessionCache.put(CacheKey(sessionId, senderKey), wrapper)
+ sessionCache.put(CacheKey(sessionId, senderKey), holder)
}
timerTask?.cancel()
@@ -96,7 +129,7 @@ internal class InboundGroupSessionStore @Inject constructor(
val toSave = mutableListOf().apply { addAll(dirtySession) }
dirtySession.clear()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
tryOrNull {
store.storeInboundGroupSessions(toSave)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index e1a706df79..501fb42db2 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -16,6 +16,11 @@
package org.matrix.android.sdk.internal.crypto
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict
@@ -40,6 +45,8 @@ import timber.log.Timber
import java.net.URLEncoder
import javax.inject.Inject
+private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
+
// The libolm wrapper.
@SessionScope
internal class MXOlmDevice @Inject constructor(
@@ -47,9 +54,12 @@ internal class MXOlmDevice @Inject constructor(
* The store where crypto data is saved.
*/
private val store: IMXCryptoStore,
+ private val olmSessionStore: OlmSessionStore,
private val inboundGroupSessionStore: InboundGroupSessionStore
) {
+ val mutex = Mutex()
+
/**
* @return the Curve25519 key for the account.
*/
@@ -93,26 +103,26 @@ internal class MXOlmDevice @Inject constructor(
try {
store.getOrCreateOlmAccount()
} catch (e: Exception) {
- Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
+ Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount")
}
try {
olmUtility = OlmUtility()
} catch (e: Exception) {
- Timber.e(e, "## MXOlmDevice : OlmUtility failed with error")
+ Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error")
olmUtility = null
}
try {
- deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
+ deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] }
} catch (e: Exception) {
- Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
+ Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
}
try {
- deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
+ deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] }
} catch (e: Exception) {
- Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
+ Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
}
}
@@ -121,9 +131,9 @@ internal class MXOlmDevice @Inject constructor(
*/
fun getOneTimeKeys(): Map>? {
try {
- return store.getOlmAccount().oneTimeKeys()
+ return store.doWithOlmAccount { it.oneTimeKeys() }
} catch (e: Exception) {
- Timber.e(e, "## getOneTimeKeys() : failed")
+ Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed")
}
return null
@@ -133,7 +143,7 @@ internal class MXOlmDevice @Inject constructor(
* @return The maximum number of one-time keys the olm account can store.
*/
fun getMaxNumberOfOneTimeKeys(): Long {
- return store.getOlmAccount().maxOneTimeKeys()
+ return store.doWithOlmAccount { it.maxOneTimeKeys() }
}
/**
@@ -143,9 +153,9 @@ internal class MXOlmDevice @Inject constructor(
*/
fun getFallbackKey(): MutableMap>? {
try {
- return store.getOlmAccount().fallbackKey()
+ return store.doWithOlmAccount { it.fallbackKey() }
} catch (e: Exception) {
- Timber.e("## getFallbackKey() : failed")
+ Timber.tag(loggerTag.value).e("## getFallbackKey() : failed")
}
return null
}
@@ -158,12 +168,14 @@ internal class MXOlmDevice @Inject constructor(
fun generateFallbackKeyIfNeeded(): Boolean {
try {
if (!hasUnpublishedFallbackKey()) {
- store.getOlmAccount().generateFallbackKey()
- store.saveOlmAccount()
+ store.doWithOlmAccount {
+ it.generateFallbackKey()
+ store.saveOlmAccount()
+ }
return true
}
} catch (e: Exception) {
- Timber.e("## generateFallbackKey() : failed")
+ Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed")
}
return false
}
@@ -174,10 +186,12 @@ internal class MXOlmDevice @Inject constructor(
fun forgetFallbackKey() {
try {
- store.getOlmAccount().forgetFallbackKey()
- store.saveOlmAccount()
+ store.doWithOlmAccount {
+ it.forgetFallbackKey()
+ store.saveOlmAccount()
+ }
} catch (e: Exception) {
- Timber.e("## forgetFallbackKey() : failed")
+ Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed")
}
}
@@ -190,6 +204,8 @@ internal class MXOlmDevice @Inject constructor(
it.groupSession.releaseSession()
}
outboundGroupSessionCache.clear()
+ inboundGroupSessionStore.clear()
+ olmSessionStore.clear()
}
/**
@@ -200,9 +216,9 @@ internal class MXOlmDevice @Inject constructor(
*/
fun signMessage(message: String): String? {
try {
- return store.getOlmAccount().signMessage(message)
+ return store.doWithOlmAccount { it.signMessage(message) }
} catch (e: Exception) {
- Timber.e(e, "## signMessage() : failed")
+ Timber.tag(loggerTag.value).e(e, "## signMessage() : failed")
}
return null
@@ -213,10 +229,12 @@ internal class MXOlmDevice @Inject constructor(
*/
fun markKeysAsPublished() {
try {
- store.getOlmAccount().markOneTimeKeysAsPublished()
- store.saveOlmAccount()
+ store.doWithOlmAccount {
+ it.markOneTimeKeysAsPublished()
+ store.saveOlmAccount()
+ }
} catch (e: Exception) {
- Timber.e(e, "## markKeysAsPublished() : failed")
+ Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed")
}
}
@@ -227,10 +245,12 @@ internal class MXOlmDevice @Inject constructor(
*/
fun generateOneTimeKeys(numKeys: Int) {
try {
- store.getOlmAccount().generateOneTimeKeys(numKeys)
- store.saveOlmAccount()
+ store.doWithOlmAccount {
+ it.generateOneTimeKeys(numKeys)
+ store.saveOlmAccount()
+ }
} catch (e: Exception) {
- Timber.e(e, "## generateOneTimeKeys() : failed")
+ Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed")
}
}
@@ -243,12 +263,14 @@ internal class MXOlmDevice @Inject constructor(
* @return the session id for the outbound session.
*/
fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
- Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
+ Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
var olmSession: OlmSession? = null
try {
olmSession = OlmSession()
- olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
+ store.doWithOlmAccount { olmAccount ->
+ olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey)
+ }
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
@@ -257,14 +279,14 @@ internal class MXOlmDevice @Inject constructor(
// this session
olmSessionWrapper.onMessageReceived()
- store.storeSession(olmSessionWrapper, theirIdentityKey)
+ olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey)
val sessionIdentifier = olmSession.sessionIdentifier()
- Timber.v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier")
+ Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier")
return sessionIdentifier
} catch (e: Exception) {
- Timber.e(e, "## createOutboundSession() failed")
+ Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed")
olmSession?.releaseSession()
}
@@ -281,34 +303,38 @@ internal class MXOlmDevice @Inject constructor(
* @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
*/
fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? {
- Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
+ Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
var olmSession: OlmSession? = null
try {
try {
olmSession = OlmSession()
- olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
+ store.doWithOlmAccount { olmAccount ->
+ olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext)
+ }
} catch (e: Exception) {
- Timber.e(e, "## createInboundSession() : the session creation failed")
+ Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed")
return null
}
- Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
+ Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
try {
- store.getOlmAccount().removeOneTimeKeys(olmSession)
- store.saveOlmAccount()
+ store.doWithOlmAccount { olmAccount ->
+ olmAccount.removeOneTimeKeys(olmSession)
+ store.saveOlmAccount()
+ }
} catch (e: Exception) {
- Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
+ Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
}
- Timber.v("## createInboundSession() : ciphertext: $ciphertext")
+ Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
try {
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
- Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256")
+ Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
} catch (e: Exception) {
- Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
+ Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
}
val olmMessage = OlmMessage()
@@ -324,9 +350,9 @@ internal class MXOlmDevice @Inject constructor(
// This counts as a received message: set last received message time to now
olmSessionWrapper.onMessageReceived()
- store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+ olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
} catch (e: Exception) {
- Timber.e(e, "## createInboundSession() : decryptMessage failed")
+ Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed")
}
val res = HashMap()
@@ -343,7 +369,7 @@ internal class MXOlmDevice @Inject constructor(
return res
} catch (e: Exception) {
- Timber.e(e, "## createInboundSession() : OlmSession creation failed")
+ Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed")
olmSession?.releaseSession()
}
@@ -357,8 +383,8 @@ internal class MXOlmDevice @Inject constructor(
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return a list of known session ids for the device.
*/
- fun getSessionIds(theirDeviceIdentityKey: String): List? {
- return store.getDeviceSessionIds(theirDeviceIdentityKey)
+ fun getSessionIds(theirDeviceIdentityKey: String): List {
+ return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey)
}
/**
@@ -368,7 +394,7 @@ internal class MXOlmDevice @Inject constructor(
* @return the session id, or null if no established session.
*/
fun getSessionId(theirDeviceIdentityKey: String): String? {
- return store.getLastUsedSessionId(theirDeviceIdentityKey)
+ return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey)
}
/**
@@ -379,30 +405,30 @@ internal class MXOlmDevice @Inject constructor(
* @param payloadString the payload to be encrypted and sent
* @return the cipher text
*/
- fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? {
- var res: MutableMap? = null
- val olmMessage: OlmMessage
+ suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? {
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
if (olmSessionWrapper != null) {
try {
- Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
- // Timber.v("## encryptMessage() : payloadString: " + payloadString);
+ Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
- olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString)
- store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
- res = HashMap()
-
- res["body"] = olmMessage.mCipherText
- res["type"] = olmMessage.mType
- } catch (e: Exception) {
- Timber.e(e, "## encryptMessage() : failed")
+ val olmMessage = olmSessionWrapper.mutex.withLock {
+ olmSessionWrapper.olmSession.encryptMessage(payloadString)
+ }
+ return mapOf(
+ "body" to olmMessage.mCipherText,
+ "type" to olmMessage.mType,
+ ).also {
+ olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+ }
+ } catch (e: Throwable) {
+ Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId")
+ return null
}
} else {
- Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+ Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+ return null
}
-
- return res
}
/**
@@ -414,7 +440,8 @@ internal class MXOlmDevice @Inject constructor(
* @param sessionId the id of the active session.
* @return the decrypted payload.
*/
- fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
+ @kotlin.jvm.Throws
+ suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
var payloadString: String? = null
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
@@ -424,13 +451,13 @@ internal class MXOlmDevice @Inject constructor(
olmMessage.mCipherText = ciphertext
olmMessage.mType = messageType.toLong()
- try {
- payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage)
- olmSessionWrapper.onMessageReceived()
- store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
- } catch (e: Exception) {
- Timber.e(e, "## decryptMessage() : decryptMessage failed")
- }
+ payloadString =
+ olmSessionWrapper.mutex.withLock {
+ olmSessionWrapper.olmSession.decryptMessage(olmMessage).also {
+ olmSessionWrapper.onMessageReceived()
+ }
+ }
+ olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
}
return payloadString
@@ -469,7 +496,7 @@ internal class MXOlmDevice @Inject constructor(
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
return session.sessionIdentifier()
} catch (e: Exception) {
- Timber.e(e, "createOutboundGroupSession")
+ Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession")
session?.releaseSession()
}
@@ -521,7 +548,7 @@ internal class MXOlmDevice @Inject constructor(
try {
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
} catch (e: Exception) {
- Timber.e(e, "## getSessionKey() : failed")
+ Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed")
}
}
return null
@@ -550,8 +577,8 @@ internal class MXOlmDevice @Inject constructor(
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
try {
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
- } catch (e: Exception) {
- Timber.e(e, "## encryptGroupMessage() : failed")
+ } catch (e: Throwable) {
+ Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed")
}
}
return null
@@ -578,52 +605,64 @@ internal class MXOlmDevice @Inject constructor(
forwardingCurve25519KeyChain: List,
keysClaimed: Map,
exportFormat: Boolean): Boolean {
- val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
- runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
- .fold(
- {
- // If we already have this session, consider updating it
- Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+ val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
+ val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+ val existingSession = existingSessionHolder?.wrapper
+ // If we have an existing one we should check if the new one is not better
+ if (existingSession != null) {
+ Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
+ try {
+ val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
+ // This is quite unexpected, could throw if native was released?
+ Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
+ candidateSession.olmInboundGroupSession?.releaseSession()
+ // Probably should discard it?
+ }
+ val newKnownFirstIndex = candidateSession.firstKnownIndex
+ // If our existing session is better we keep it
+ if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
+ Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
+ candidateSession.olmInboundGroupSession?.releaseSession()
+ return false
+ }
+ } catch (failure: Throwable) {
+ Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
+ candidateSession.olmInboundGroupSession?.releaseSession()
+ return false
+ }
+ }
- val existingFirstKnown = it.firstKnownIndex!!
- val newKnownFirstIndex = session.firstKnownIndex
+ Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
- // If our existing session is better we keep it
- if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
- session.olmInboundGroupSession?.releaseSession()
- return false
- }
- },
- {
- // Nothing to do in case of error
- }
- )
-
- // sanity check
- if (null == session.olmInboundGroupSession) {
- Timber.e("## addInboundGroupSession : invalid session")
+ // sanity check on the new session
+ val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
+ if (null == candidateOlmInboundSession) {
+ Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ")
return false
}
try {
- if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
- Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
- session.olmInboundGroupSession!!.releaseSession()
+ if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
+ Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+ candidateOlmInboundSession.releaseSession()
return false
}
- } catch (e: Exception) {
- session.olmInboundGroupSession?.releaseSession()
- Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
+ } catch (e: Throwable) {
+ candidateOlmInboundSession.releaseSession()
+ Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
return false
}
- session.senderKey = senderKey
- session.roomId = roomId
- session.keysClaimed = keysClaimed
- session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
+ candidateSession.senderKey = senderKey
+ candidateSession.roomId = roomId
+ candidateSession.keysClaimed = keysClaimed
+ candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
- inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
-// store.storeInboundGroupSessions(listOf(session))
+ if (existingSession != null) {
+ inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+ } else {
+ inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+ }
return true
}
@@ -638,57 +677,70 @@ internal class MXOlmDevice @Inject constructor(
val sessions = ArrayList(megolmSessionsData.size)
for (megolmSessionData in megolmSessionsData) {
- val sessionId = megolmSessionData.sessionId
- val senderKey = megolmSessionData.senderKey
+ val sessionId = megolmSessionData.sessionId ?: continue
+ val senderKey = megolmSessionData.senderKey ?: continue
val roomId = megolmSessionData.roomId
- var session: OlmInboundGroupSessionWrapper2? = null
+ var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
try {
- session = OlmInboundGroupSessionWrapper2(megolmSessionData)
+ candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
} catch (e: Exception) {
- Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+ Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
}
// sanity check
- if (session?.olmInboundGroupSession == null) {
- Timber.e("## importInboundGroupSession : invalid session")
+ if (candidateSessionToImport?.olmInboundGroupSession == null) {
+ Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
continue
}
+ val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
try {
- if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
- Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
- if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
+ if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
+ Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+ candidateOlmInboundGroupSession?.releaseSession()
continue
}
} catch (e: Exception) {
- Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed")
- session.olmInboundGroupSession!!.releaseSession()
+ Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
+ candidateOlmInboundGroupSession?.releaseSession()
continue
}
- runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
- .fold(
- {
- // If we already have this session, consider updating it
- Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+ val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+ val existingSession = existingSessionHolder?.wrapper
- // For now we just ignore updates. TODO: implement something here
- if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
- // Ignore this, keep existing
- session.olmInboundGroupSession!!.releaseSession()
- } else {
- sessions.add(session)
- }
- Unit
- },
- {
- // Session does not already exist, add it
- sessions.add(session)
- }
+ if (existingSession == null) {
+ // Session does not already exist, add it
+ Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId")
+ sessions.add(candidateSessionToImport)
+ } else {
+ Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+ val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
+ val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
- )
+ if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
+ // should not happen?
+ candidateSessionToImport.olmInboundGroupSession?.releaseSession()
+ Timber.tag(loggerTag.value)
+ .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
+ } else {
+ if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
+ // Ignore this, keep existing
+ candidateOlmInboundGroupSession.releaseSession()
+ } else {
+ // update cache with better session
+ inboundGroupSessionStore.replaceGroupSession(
+ existingSessionHolder,
+ InboundGroupSessionHolder(candidateSessionToImport),
+ sessionId,
+ senderKey
+ )
+ sessions.add(candidateSessionToImport)
+ }
+ }
+ }
}
store.storeInboundGroupSessions(sessions)
@@ -696,18 +748,6 @@ internal class MXOlmDevice @Inject constructor(
return sessions
}
- /**
- * Remove an inbound group session
- *
- * @param sessionId the session identifier.
- * @param sessionKey base64-encoded secret key.
- */
- fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) {
- if (null != sessionId && null != sessionKey) {
- store.removeInboundGroupSession(sessionId, sessionKey)
- }
- }
-
/**
* Decrypt a received message with an inbound group session.
*
@@ -719,19 +759,24 @@ internal class MXOlmDevice @Inject constructor(
* @return the decrypting result. Nil if the sessionId is unknown.
*/
@Throws(MXCryptoError::class)
- fun decryptGroupMessage(body: String,
- roomId: String,
- timeline: String?,
- sessionId: String,
- senderKey: String): OlmDecryptionResult {
- val session = getInboundGroupSession(sessionId, senderKey, roomId)
+ suspend fun decryptGroupMessage(body: String,
+ roomId: String,
+ timeline: String?,
+ sessionId: String,
+ senderKey: String): OlmDecryptionResult {
+ val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
+ val wrapper = sessionHolder.wrapper
+ val inboundGroupSession = wrapper.olmInboundGroupSession
+ ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
- if (roomId == session.roomId) {
+ if (roomId == wrapper.roomId) {
val decryptResult = try {
- session.olmInboundGroupSession!!.decryptMessage(body)
+ sessionHolder.mutex.withLock {
+ inboundGroupSession.decryptMessage(body)
+ }
} catch (e: OlmException) {
- Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
+ Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
throw MXCryptoError.OlmError(e)
}
@@ -742,32 +787,32 @@ internal class MXOlmDevice @Inject constructor(
if (timelineSet.contains(messageIndexKey)) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
- Timber.e("## decryptGroupMessage() : $reason")
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
}
timelineSet.add(messageIndexKey)
}
- inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
+ inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
adapter.fromJson(payloadString)
} catch (e: Exception) {
- Timber.e("## decryptGroupMessage() : fails to parse the payload")
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
return OlmDecryptionResult(
payload,
- session.keysClaimed,
+ wrapper.keysClaimed,
senderKey,
- session.forwardingCurve25519KeyChain
+ wrapper.forwardingCurve25519KeyChain
)
} else {
- val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
- Timber.e("## decryptGroupMessage() : $reason")
+ val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
}
}
@@ -819,7 +864,7 @@ internal class MXOlmDevice @Inject constructor(
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
// sanity check
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
- store.getDeviceSession(sessionId, theirDeviceIdentityKey)
+ olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey)
}
}
@@ -832,25 +877,26 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the inbound group session.
*/
- fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 {
+ fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
}
- val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+ val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+ val session = holder?.wrapper
if (session != null) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId != session.roomId) {
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
- Timber.e("## getInboundGroupSession() : $errorDescription")
+ Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
} else {
- return session
+ return holder
}
} else {
- Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
+ Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
}
}
@@ -866,4 +912,9 @@ internal class MXOlmDevice @Inject constructor(
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
}
+
+ @VisibleForTesting
+ fun clearOlmSessionCache() {
+ olmSessionStore.clear()
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
new file mode 100644
index 0000000000..f4fbca6a0f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2022 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.crypto
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.olm.OlmSession
+import timber.log.Timber
+import javax.inject.Inject
+
+private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
+
+/**
+ * Keep the used olm session in memory and load them from the data layer when needed
+ * Access is synchronized for thread safety
+ */
+internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
+ /**
+ * map of device key to list of olm sessions (it is possible to have several active sessions with a device)
+ */
+ private val olmSessions = HashMap>()
+
+ /**
+ * Store a session between our own device and another device.
+ * This will be called after the session has been created but also every time it has been used
+ * in order to persist the correct state for next run
+ * @param olmSessionWrapper the end-to-end session.
+ * @param deviceKey the public key of the other device.
+ */
+ @Synchronized
+ fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
+ // This could be a newly created session or one that was just created
+ // Anyhow we should persist ratchet state for future app lifecycle
+ addNewSessionInCache(olmSessionWrapper, deviceKey)
+ store.storeSession(olmSessionWrapper, deviceKey)
+ }
+
+ /**
+ * Get all the Olm Sessions we are sharing with the given device.
+ *
+ * @param deviceKey the public key of the other device.
+ * @return A set of sessionId, or empty if device is not known
+ */
+ @Synchronized
+ fun getDeviceSessionIds(deviceKey: String): List {
+ // we need to get the persisted ids first
+ val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
+ .orEmpty()
+ .toMutableList()
+ // Do we have some in cache not yet persisted?
+ olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
+ getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
+ if (!persistedKnownSessions.contains(cachedSessionId)) {
+ persistedKnownSessions.add(cachedSessionId)
+ }
+ }
+ }
+ return persistedKnownSessions
+ }
+
+ /**
+ * Retrieve an end-to-end session between our own device and another
+ * device.
+ *
+ * @param sessionId the session Id.
+ * @param deviceKey the public key of the other device.
+ * @return the session wrapper if found
+ */
+ @Synchronized
+ fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+ // get from cache or load and add to cache
+ return internalGetSession(sessionId, deviceKey)
+ }
+
+ /**
+ * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist
+ *
+ * @param deviceKey the public key of the other device.
+ * @return last used sessionId, or null if not found
+ */
+ @Synchronized
+ fun getLastUsedSessionId(deviceKey: String): String? {
+ // We want to avoid to load in memory old session if possible
+ val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
+ var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
+ // we should check if we have one in cache with a higher last message received?
+ olmSessions[deviceKey].orEmpty().forEach { inCache ->
+ if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
+ candidate = inCache
+ }
+ }
+
+ return candidate?.olmSession?.sessionIdentifier()
+ }
+
+ /**
+ * Release all sessions and clear cache
+ */
+ @Synchronized
+ fun clear() {
+ olmSessions.entries.onEach { entry ->
+ entry.value.onEach { it.olmSession.releaseSession() }
+ }
+ olmSessions.clear()
+ }
+
+ private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+ return getSessionInCache(sessionId, deviceKey)
+ ?: // deserialize from store
+ return store.getDeviceSession(sessionId, deviceKey)?.also {
+ addNewSessionInCache(it, deviceKey)
+ }
+ }
+
+ private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+ return olmSessions[deviceKey]?.firstOrNull {
+ getSafeSessionIdentifier(it.olmSession) == sessionId
+ }
+ }
+
+ private fun getSafeSessionIdentifier(session: OlmSession): String? {
+ return try {
+ session.sessionIdentifier()
+ } catch (throwable: Throwable) {
+ Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
+ null
+ }
+ }
+
+ private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
+ val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
+ olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
+ val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
+ it.add(session)
+ // remove and release if was there but with different instance
+ if (existing != null && existing.olmSession != session.olmSession) {
+ // mm not sure when this could happen
+ // anyhow we should remove and release the one known
+ it.remove(existing)
+ existing.olmSession.releaseSession()
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
index ab2ed04dfb..87c176612d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
@@ -16,14 +16,18 @@
package org.matrix.android.sdk.internal.crypto.actions
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
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.MXKey
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.toDebugString
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
+import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
@@ -31,90 +35,90 @@ private const val ONE_TIME_KEYS_RETRY_COUNT = 3
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
+@SessionScope
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
private val olmDevice: MXOlmDevice,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
+ private val ensureMutex = Mutex()
+
+ /**
+ * We want to synchronize a bit here, because we are iterating to check existing olm session and
+ * also adding some
+ */
suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap {
- val devicesWithoutSession = ArrayList()
+ ensureMutex.withLock {
+ val results = MXUsersDevicesMap()
+ val deviceList = devicesByUser.flatMap { it.value }
+ Timber.tag(loggerTag.value)
+ .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
+ val devicesToCreateSessionWith = mutableListOf()
+ if (force) {
+ // we take all devices and will query otk for them
+ devicesToCreateSessionWith.addAll(deviceList)
+ } else {
+ // only peek devices without active session
+ deviceList.forEach { deviceInfo ->
+ val deviceId = deviceInfo.deviceId
+ val userId = deviceInfo.userId
+ val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
+ Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
+ }
- val results = MXUsersDevicesMap()
-
- for ((userId, deviceList) in devicesByUser) {
- for (deviceInfo in deviceList) {
- val deviceId = deviceInfo.deviceId
- val key = deviceInfo.identityKey()
- if (key == null) {
- Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key")
- continue
- }
-
- val sessionId = olmDevice.getSessionId(key)
-
- if (sessionId.isNullOrEmpty() || force) {
- Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)")
- devicesWithoutSession.add(deviceInfo)
- } else {
- Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
- }
-
- val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
- results.setObject(userId, deviceId, olmSessionResult)
- }
- }
-
- Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" +
- " ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}")
- if (devicesWithoutSession.size == 0) {
- return results
- }
-
- // Prepare the request for claiming one-time keys
- val usersDevicesToClaim = MXUsersDevicesMap()
-
- val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
-
- for (device in devicesWithoutSession) {
- usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm)
- }
-
- // TODO: this has a race condition - if we try to send another message
- // while we are claiming a key, we will end up claiming two and setting up
- // two sessions.
- //
- // That should eventually resolve itself, but it's poor form.
-
- Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}")
-
- val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
- val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT)
- Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
- for ((userId, deviceInfos) in devicesByUser) {
- for (deviceInfo in deviceInfos) {
- var oneTimeKey: MXKey? = null
- val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
- if (null != deviceIds) {
- for (deviceId in deviceIds) {
- val olmSessionResult = results.getObject(userId, deviceId)
- if (olmSessionResult?.sessionId != null && !force) {
- // We already have a result for this device
- continue
- }
- val key = oneTimeKeys.getObject(userId, deviceId)
- if (key?.type == oneTimeKeyAlgorithm) {
- oneTimeKey = key
- }
- if (oneTimeKey == null) {
- Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId")
- continue
- }
- // Update the result for this device in results
- olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
+ // is there a session that as been already used?
+ val sessionId = olmDevice.getSessionId(key)
+ if (sessionId.isNullOrEmpty()) {
+ Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
+ devicesToCreateSessionWith.add(deviceInfo)
+ } else {
+ Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
+ val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
+ results.setObject(userId, deviceId, olmSessionResult)
}
}
}
+
+ if (devicesToCreateSessionWith.isEmpty()) {
+ // no session to create
+ return results
+ }
+ val usersDevicesToClaim = MXUsersDevicesMap().apply {
+ devicesToCreateSessionWith.forEach {
+ setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
+ }
+ }
+
+ // Let's now claim one time keys
+ val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
+ val oneTimeKeys = withContext(coroutineDispatchers.io) {
+ oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
+ }
+
+ // let now start olm session using the new otks
+ devicesToCreateSessionWith.forEach { deviceInfo ->
+ val userId = deviceInfo.userId
+ val deviceId = deviceInfo.deviceId
+ // Did we get an OTK
+ val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
+ if (oneTimeKey == null) {
+ Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
+ } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
+ Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
+ } else {
+ val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
+ if (olmSessionId != null) {
+ val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
+ results.setObject(userId, deviceId, olmSessionResult)
+ } else {
+ Timber
+ .tag(loggerTag.value)
+ .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
+ }
+ }
+ }
+ return results
}
- return results
}
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
index 165f200bac..4e158602c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.actions
+import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -28,6 +29,8 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
import timber.log.Timber
import javax.inject.Inject
+private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
+
internal class MessageEncrypter @Inject constructor(
@UserId
private val userId: String,
@@ -42,7 +45,7 @@ internal class MessageEncrypter @Inject constructor(
* @param deviceInfos list of device infos to encrypt for.
* @return the content for an m.room.encrypted event.
*/
- fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage {
+ suspend fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage {
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
val payloadJson = payloadFields.toMutableMap()
@@ -66,7 +69,7 @@ internal class MessageEncrypter @Inject constructor(
val sessionId = olmDevice.getSessionId(deviceKey)
if (!sessionId.isNullOrEmpty()) {
- Timber.v("Using sessionid $sessionId for device $deviceKey")
+ Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
payloadJson["recipient"] = deviceInfo.userId
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
index 79c7608cbf..b6c1d99aa5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
@@ -36,7 +36,7 @@ internal interface IMXDecrypting {
* @return the decryption information, or an error
*/
@Throws(MXCryptoError::class)
- fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+ suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/**
* Handle a key event.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
index 1fd5061a65..6f488def0a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
@@ -45,7 +45,7 @@ internal interface IMXGroupEncryption {
*
* @return true in case of success
*/
- suspend fun reshareKey(sessionId: String,
+ suspend fun reshareKey(groupSessionId: String,
userId: String,
deviceId: String,
senderKey: String): Boolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 2ee24dfbb0..e94daa0e76 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -71,7 +72,7 @@ internal class MXMegolmDecryption(private val userId: String,
// private var pendingEvents: MutableMap>> = HashMap()
@Throws(MXCryptoError::class)
- override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
@@ -79,7 +80,7 @@ internal class MXMegolmDecryption(private val userId: String,
}
@Throws(MXCryptoError::class)
- private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
+ private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
@@ -345,7 +346,22 @@ internal class MXMegolmDecryption(private val userId: String,
return
}
val userId = request.userId ?: return
+
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ val body = request.requestBody
+ val sessionHolder = try {
+ olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
+ } catch (failure: Throwable) {
+ Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
+ return@launch
+ }
+
+ val export = sessionHolder.mutex.withLock {
+ sessionHolder.wrapper.exportKeys()
+ } ?: return@launch Unit.also {
+ Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
+ }
+
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching {
val deviceId = request.deviceId
@@ -355,7 +371,6 @@ internal class MXMegolmDecryption(private val userId: String,
} else {
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
- val body = request.requestBody
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there
@@ -365,19 +380,10 @@ internal class MXMegolmDecryption(private val userId: String,
}
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
- val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY)
- runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) }
- .fold(
- {
- // TODO
- payloadJson["content"] = it.exportKeys() ?: ""
- },
- {
- // TODO
- Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body")
- }
-
- )
+ val payloadJson = mapOf(
+ "type" to EventType.FORWARDED_ROOM_KEY,
+ "content" to export
+ )
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index 389036a1f8..cf9733dc2d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -88,7 +90,7 @@ internal class MXMegolmEncryption(
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds)
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}")
+ Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
val outboundSession = ensureOutboundSession(devices.allowedDevices)
return encryptContent(outboundSession, eventType, eventContent)
@@ -142,8 +144,9 @@ internal class MXMegolmEncryption(
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
- val keysClaimedMap = HashMap()
- keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
+ val keysClaimedMap = mapOf(
+ "ed25519" to olmDevice.deviceEd25519Key!!
+ )
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
emptyList(), keysClaimedMap, false)
@@ -303,11 +306,13 @@ internal class MXMegolmEncryption(
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
try {
- sendToDeviceTask.execute(sendToDeviceParams)
+ withContext(coroutineDispatchers.io) {
+ sendToDeviceTask.execute(sendToDeviceParams)
+ }
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
} catch (failure: Throwable) {
// What to do here...
- Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
+ Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
}
} else {
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
@@ -346,9 +351,12 @@ internal class MXMegolmEncryption(
}
)
try {
- sendToDeviceTask.execute(params)
+ withContext(coroutineDispatchers.io) {
+ sendToDeviceTask.execute(params)
+ }
} catch (failure: Throwable) {
- Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
+ Timber.tag(loggerTag.value)
+ .e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}")
}
}
@@ -432,20 +440,20 @@ internal class MXMegolmEncryption(
}
}
- override suspend fun reshareKey(sessionId: String,
+ override suspend fun reshareKey(groupSessionId: String,
userId: String,
deviceId: String,
senderKey: String): Boolean {
- Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId")
+ Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
// Get the chain index of the key we previously sent this device
- val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo)
+ val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
if (!wasSessionSharedWithUser.found) {
// This session was never shared with this user
// Send a room key with held
- notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
+ notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
return false
}
@@ -456,42 +464,47 @@ internal class MXMegolmEncryption(
}
val devicesByUser = mapOf(userId to listOf(deviceInfo))
- val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
- val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
- 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.
- ?: return false.also {
- Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
- }
+ val usersDeviceMap = try {
+ ensureOlmSessionsForDevicesAction.handle(devicesByUser)
+ } catch (failure: Throwable) {
+ null
+ }
+ val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
+ if (olmSessionResult?.sessionId == null) {
+ Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
+ return false
+ }
+ Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
- Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
+ val sessionHolder = try {
+ olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
+ } catch (failure: Throwable) {
+ Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
+ return false
+ }
- val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY)
+ val export = sessionHolder.mutex.withLock {
+ sessionHolder.wrapper.exportKeys()
+ } ?: return false.also {
+ Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
+ }
- runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
- .fold(
- {
- // TODO
- payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
- },
- {
- // TODO
- Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId")
- }
-
- )
+ val payloadJson = mapOf(
+ "type" to EventType.FORWARDED_ROOM_KEY,
+ "content" to export
+ )
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
- Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId")
+ Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
return try {
sendToDeviceTask.execute(sendToDeviceParams)
- Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId")
+ Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
true
} catch (failure: Throwable) {
- Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
+ Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
false
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
index f1bca4fbc6..afa249801d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
@@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.crypto.algorithms.olm
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.logger.LoggerTag
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.toModel
@@ -30,6 +32,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.convertFromUTF8
import timber.log.Timber
+private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
internal class MXOlmDecryption(
// The olm device interface
private val olmDevice: MXOlmDevice,
@@ -38,27 +41,27 @@ internal class MXOlmDecryption(
IMXDecrypting {
@Throws(MXCryptoError::class)
- override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val olmEventContent = event.content.toModel() ?: run {
- Timber.e("## decryptEvent() : bad event format")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
}
val cipherText = olmEventContent.ciphertext ?: run {
- Timber.e("## decryptEvent() : missing cipher text")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
}
val senderKey = olmEventContent.senderKey ?: run {
- Timber.e("## decryptEvent() : missing sender key")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
}
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
- Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
}
@@ -69,7 +72,7 @@ internal class MXOlmDecryption(
val decryptedPayload = decryptMessage(message, senderKey)
if (decryptedPayload == null) {
- Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
+ Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
}
val payloadString = convertFromUTF8(decryptedPayload)
@@ -78,30 +81,30 @@ internal class MXOlmDecryption(
val payload = adapter.fromJson(payloadString)
if (payload == null) {
- Timber.e("## decryptEvent failed : null payload")
+ Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
}
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
- Timber.e("## decryptEvent() : bad olmPayloadContent format")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
if (olmPayloadContent.recipient.isNullOrBlank()) {
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
- Timber.e("## decryptEvent() : $reason")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
}
if (olmPayloadContent.recipient != userId) {
- Timber.e("## decryptEvent() : Event ${event.eventId}:" +
+ Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
}
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
- Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
+ Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
" property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
@@ -110,31 +113,34 @@ internal class MXOlmDecryption(
val ed25519 = recipientKeys["ed25519"]
if (ed25519 != olmDevice.deviceEd25519Key) {
- Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
+ Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
MXCryptoError.BAD_RECIPIENT_KEY_REASON)
}
if (olmPayloadContent.sender.isNullOrBlank()) {
- Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
+ Timber.tag(loggerTag.value)
+ .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
}
if (olmPayloadContent.sender != event.senderId) {
- Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
+ Timber.tag(loggerTag.value)
+ .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
}
if (olmPayloadContent.roomId != event.roomId) {
- Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
+ Timber.tag(loggerTag.value)
+ .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
}
val keys = olmPayloadContent.keys ?: run {
- Timber.e("## decryptEvent failed : null keys")
+ Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
}
@@ -153,8 +159,8 @@ internal class MXOlmDecryption(
* @param message message object, with 'type' and 'body' fields.
* @return payload, if decrypted successfully.
*/
- private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
- val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
+ private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
+ val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
val messageBody = message["body"] as? String ?: return null
val messageType = when (val typeAsVoid = message["type"]) {
@@ -166,11 +172,32 @@ internal class MXOlmDecryption(
// Try each session in turn
// decryptionErrors = {};
+
+ val isPreKey = messageType == 0
+ // we want to synchronize on prekey if not we could end up create two olm sessions
+ // Not very clear but it looks like the js-sdk for consistency
+ return if (isPreKey) {
+ olmDevice.mutex.withLock {
+ reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+ }
+ } else {
+ reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+ }
+ }
+
+ private suspend fun reallyDecryptMessage(sessionIds: List, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
+ Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
for (sessionId in sessionIds) {
- val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+ val payload = try {
+ olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+ } catch (throwable: Exception) {
+ // As we are trying one by one, we don't really care of the error here
+ Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
+ null
+ }
if (null != payload) {
- Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
+ Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
return payload
} else {
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
@@ -178,7 +205,7 @@ internal class MXOlmDecryption(
if (foundSession) {
// Decryption failed, but it was a prekey message matching this
// session, so it should have worked.
- Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
+ Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
return null
}
}
@@ -189,9 +216,9 @@ internal class MXOlmDecryption(
// didn't work.
if (sessionIds.isEmpty()) {
- Timber.e("## decryptMessage() : No existing sessions")
+ Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
} else {
- Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
+ Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
}
return null
@@ -199,14 +226,17 @@ internal class MXOlmDecryption(
// prekey message which doesn't match any existing sessions: make a new
// session.
+ // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
+ Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey")
+
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
if (null == res) {
- Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
+ Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
return null
}
- Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
+ Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
return res["payload"]
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
index 5cd647ff6f..9325355d28 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -96,7 +96,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
if (userList.isNotEmpty()) {
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/
- Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
+ Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
updateTrust(userList)
}
@@ -148,7 +148,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
- Timber.d("## CrossSigning - user:${entry.key} result:$it")
+ Timber.v("## CrossSigning - user:${entry.key} result:$it")
}
}
}
@@ -178,7 +178,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
- Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
+ Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
@@ -216,7 +216,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
.findFirst()
?.let { roomSummary ->
- Timber.d("## CrossSigning - Check shield state for room $roomId")
+ Timber.v("## CrossSigning - Check shield state for room $roomId")
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeRoomShield(
@@ -277,7 +277,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
cryptoRealm: Realm,
activeMemberUserIds: List,
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
- Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
+ Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
// The set of “all users” depends on the type of room:
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index b20168eaa3..954c2dbe43 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -671,7 +671,6 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
throw InvalidParameterException("Invalid recovery key")
}
-
// Get a PK decryption instance
pkDecryptionFromRecoveryKey(recoveryKey)
}
@@ -681,6 +680,10 @@ internal class DefaultKeysBackupService @Inject constructor(
throw InvalidParameterException("Invalid recovery key")
}
+ // Save for next time and for gossiping
+ // Save now as it's valid, don't wait for the import as it could take long.
+ saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
+
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
// Get backed up keys from the homeserver
@@ -729,8 +732,6 @@ internal class DefaultKeysBackupService @Inject constructor(
if (backUp) {
maybeBackupKeys()
}
- // Save for next time and for gossiping
- saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
result
}
}.foldToCallback(callback)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
index 5e7744853a..b3638dc414 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
@@ -70,6 +70,8 @@ data class CryptoDeviceInfo(
keys?.let { map["keys"] = it }
return map
}
+
+ fun shortDebugString() = "$userId|$deviceId"
}
internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
index 15b92f105a..263cb3b036 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.model
+import kotlinx.coroutines.sync.Mutex
import org.matrix.olm.OlmSession
/**
@@ -25,7 +26,10 @@ data class OlmSessionWrapper(
// The associated olm session.
val olmSession: OlmSession,
// Timestamp at which the session last received a message.
- var lastReceivedMessageTs: Long = 0) {
+ var lastReceivedMessageTs: Long = 0,
+
+ val mutex: Mutex = Mutex()
+) {
/**
* Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs`
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 96ea5c03fa..e662ff74e7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -54,7 +54,7 @@ internal interface IMXCryptoStore {
/**
* @return the olm account
*/
- fun getOlmAccount(): OlmAccount
+ fun doWithOlmAccount(block: (OlmAccount) -> T): T
fun getOrCreateOlmAccount(): OlmAccount
@@ -261,7 +261,7 @@ internal interface IMXCryptoStore {
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
/**
- * Retrieve the end-to-end session ids between the logged-in user and another
+ * Retrieve all end-to-end session ids between our own device and another
* device.
*
* @param deviceKey the public key of the other device.
@@ -270,7 +270,7 @@ internal interface IMXCryptoStore {
fun getDeviceSessionIds(deviceKey: String): List?
/**
- * Retrieve an end-to-end session between the logged-in user and another
+ * Retrieve an end-to-end session between our own device and another
* device.
*
* @param sessionId the session Id.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index a07827c033..585b3d2d25 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -104,7 +104,6 @@ import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
-import kotlin.collections.set
@SessionScope
internal class RealmCryptoStore @Inject constructor(
@@ -124,12 +123,6 @@ internal class RealmCryptoStore @Inject constructor(
// The olm account
private var olmAccount: OlmAccount? = null
- // Cache for OlmSession, to release them properly
- private val olmSessionsToRelease = HashMap()
-
- // Cache for InboundGroupSession, to release them properly
- private val inboundGroupSessionToRelease = HashMap()
-
private val newSessionListeners = ArrayList()
override fun addNewSessionListener(listener: NewSessionListener) {
@@ -213,16 +206,6 @@ internal class RealmCryptoStore @Inject constructor(
monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
}
- olmSessionsToRelease.forEach {
- it.value.olmSession.releaseSession()
- }
- olmSessionsToRelease.clear()
-
- inboundGroupSessionToRelease.forEach {
- it.value.olmInboundGroupSession?.releaseSession()
- }
- inboundGroupSessionToRelease.clear()
-
olmAccount?.releaseAccount()
realmLocker?.close()
@@ -247,10 +230,18 @@ internal class RealmCryptoStore @Inject constructor(
}
}
- override fun getOlmAccount(): OlmAccount {
- return olmAccount!!
+ /**
+ * Olm account access should be synchronized
+ */
+ override fun doWithOlmAccount(block: (OlmAccount) -> T): T {
+ return olmAccount!!.let { olmAccount ->
+ synchronized(olmAccount) {
+ block.invoke(olmAccount)
+ }
+ }
}
+ @Synchronized
override fun getOrCreateOlmAccount(): OlmAccount {
doRealmTransaction(realmConfiguration) {
val metaData = it.where().findFirst()
@@ -680,13 +671,6 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) {
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
- // Release memory of previously known session, if it is not the same one
- if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) {
- olmSessionsToRelease[key]?.olmSession?.releaseSession()
- }
-
- olmSessionsToRelease[key] = olmSessionWrapper
-
doRealmTransaction(realmConfiguration) {
val realmOlmSession = OlmSessionEntity().apply {
primaryKey = key
@@ -703,23 +687,18 @@ internal class RealmCryptoStore @Inject constructor(
override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
-
- // If not in cache (or not found), try to read it from realm
- if (olmSessionsToRelease[key] == null) {
- doRealmQueryAndCopy(realmConfiguration) {
- it.where()
- .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
- .findFirst()
- }
- ?.let {
- val olmSession = it.getOlmSession()
- if (olmSession != null && it.sessionId != null) {
- olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
- }
- }
+ return doRealmQueryAndCopy(realmConfiguration) {
+ it.where()
+ .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
+ .findFirst()
}
-
- return olmSessionsToRelease[key]
+ ?.let {
+ val olmSession = it.getOlmSession()
+ if (olmSession != null && it.sessionId != null) {
+ return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
+ }
+ null
+ }
}
override fun getLastUsedSessionId(deviceKey: String): String? {
@@ -761,13 +740,6 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
- // Release memory of previously known session, if it is not the same one
- if (inboundGroupSessionToRelease[key] != session) {
- inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
- }
-
- inboundGroupSessionToRelease[key] = session
-
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
@@ -784,20 +756,12 @@ internal class RealmCryptoStore @Inject constructor(
override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
- // If not in cache (or not found), try to read it from realm
- if (inboundGroupSessionToRelease[key] == null) {
- doWithRealm(realmConfiguration) {
- it.where()
- .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
- .findFirst()
- ?.getInboundGroupSession()
- }
- ?.let {
- inboundGroupSessionToRelease[key] = it
- }
+ return doWithRealm(realmConfiguration) {
+ it.where()
+ .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
+ .findFirst()
+ ?.getInboundGroupSession()
}
-
- return inboundGroupSessionToRelease[key]
}
override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
@@ -853,10 +817,6 @@ internal class RealmCryptoStore @Inject constructor(
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
- // Release memory of previously known session
- inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
- inboundGroupSessionToRelease.remove(key)
-
doRealmTransaction(realmConfiguration) {
it.where()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
index 829e066bf3..90ede18dc8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
@@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction
import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart
-import org.matrix.android.sdk.internal.util.exhaustive
import timber.log.Timber
internal class DefaultQrCodeVerificationTransaction(
@@ -129,7 +128,7 @@ internal class DefaultQrCodeVerificationTransaction(
// Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK
}
}
- }.exhaustive
+ }
val toVerifyDeviceIds = mutableListOf()
@@ -174,7 +173,7 @@ internal class DefaultQrCodeVerificationTransaction(
Unit
}
}
- }.exhaustive
+ }
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// Nothing to verify
@@ -272,6 +271,7 @@ internal class DefaultQrCodeVerificationTransaction(
// I now know that i can trust my MSK
trust(true, emptyList(), true)
}
+ null -> Unit
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 12e60da114..a57397dad5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber
import javax.inject.Inject
@@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000
- val schemaVersion = 25L
+ val schemaVersion = 26L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 23) MigrateSessionTo023(realm).perform()
if (oldVersion < 24) MigrateSessionTo024(realm).perform()
if (oldVersion < 25) MigrateSessionTo025(realm).perform()
+ if (oldVersion < 26) MigrateSessionTo026(realm).perform()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 289db9fa15..d2e3e99b75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
- roomMemberContentsByUser: Map? = null) {
+ ownedByThreadChunk: Boolean = false,
+ roomMemberContentsByUser: Map? = null): TimelineEventEntity? {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
- return
+ return null
}
val displayIndex = nextDisplayIndex(direction)
val localId = TimelineEventEntity.nextId(realm)
val senderId = eventEntity.sender ?: ""
// Update RR for the sender of a new message with a dummy one
- val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
+ val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
val timelineEventEntity = realm.createObject().apply {
this.localId = localId
this.root = eventEntity
@@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
?.also { it.cleanUp(eventEntity.sender) }
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
+ this.ownedByThreadChunk = ownedByThreadChunk
val roomMemberContent = roomMemberContentsByUser?.get(senderId)
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
@@ -113,9 +115,10 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
}
// numberOfTimelineEvents++
timelineEvents.add(timelineEventEntity)
+ return timelineEventEntity
}
-private fun computeIsUnique(
+fun computeIsUnique(
realm: Realm,
roomId: String,
isLastForward: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
index 724f307e3b..9ad2708b43 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
@@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
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.threads.ThreadSummaryEntity
internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
if (!chunks.contains(chunkEntity)) {
chunks.add(chunkEntity)
}
}
+
+internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
+ if (!threadSummaries.contains(threadSummary)) {
+ threadSummaries.add(threadSummary)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index f703bfaf82..ee3008d40b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
-private typealias ThreadSummary = Pair?
+private typealias Summary = Pair?
/**
* Finds the root thread event and update it with the latest message summary along with the number
@@ -93,11 +93,12 @@ internal fun EventEntity.markEventAsRoot(
* @param rootThreadEventId The root eventId that will find the number of threads
* @return A ThreadSummary containing the counted threads and the latest event message
*/
-internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
+internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
// Number of messages
val messages = TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+ .distinct(TimelineEventEntityFields.ROOT.EVENT_ID)
.count()
.toInt()
@@ -123,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
result ?: return null
- return ThreadSummary(messages, result)
+ return Summary(messages, result)
}
/**
@@ -156,6 +157,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+ .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
.sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
new file mode 100644
index 0000000000..7087f07162
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2022 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.database.helper
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.Sort
+import io.realm.kotlin.createObject
+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.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+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.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
+import timber.log.Timber
+import java.util.UUID
+
+internal fun ThreadSummaryEntity.updateThreadSummary(
+ rootThreadEventEntity: EventEntity,
+ numberOfThreads: Int?,
+ latestThreadEventEntity: EventEntity?,
+ isUserParticipating: Boolean,
+ roomMemberContentsByUser: HashMap) {
+ updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
+ updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
+ this.isUserParticipating = isUserParticipating
+ numberOfThreads?.let {
+ // Update number of threads only when there is an actual value
+ this.numberOfThreads = it
+ }
+}
+
+/**
+ * Updates the root thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
+ rootThreadEventEntity: EventEntity,
+ roomMemberContentsByUser: HashMap
+) {
+ val roomId = rootThreadEventEntity.roomId
+ val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
+ this.rootThreadEventEntity = rootThreadEventEntity
+ this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
+ this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
+ this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
+ computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
+ } else {
+ true
+ }
+}
+
+/**
+ * Updates the latest thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
+ latestThreadEventEntity: EventEntity?,
+ roomMemberContentsByUser: HashMap
+) {
+ val roomId = latestThreadEventEntity?.roomId ?: return
+ val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
+ this.latestThreadEventEntity = latestThreadEventEntity
+ this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
+ this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
+ this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
+ computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
+ } else {
+ true
+ }
+}
+
+private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap): TimelineEventEntity {
+ val roomId = roomId
+ val eventId = eventId
+ val localId = TimelineEventEntity.nextId(realm)
+ val senderId = sender ?: ""
+
+ val timelineEventEntity = realm.createObject().apply {
+ this.localId = localId
+ this.root = this@toTimelineEventEntity
+ this.eventId = eventId
+ this.roomId = roomId
+ this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
+ ?.also { it.cleanUp(sender) }
+ this.ownedByThreadChunk = true // To skip it from the original event flow
+ val roomMemberContent = roomMemberContentsByUser[senderId]
+ this.senderAvatar = roomMemberContent?.avatarUrl
+ this.senderName = roomMemberContent?.displayName
+ isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
+ computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
+ } else {
+ true
+ }
+ }
+ return timelineEventEntity
+}
+
+internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
+ threadSummaryType: ThreadSummaryUpdateType,
+ realm: Realm,
+ roomId: String,
+ threadEventEntity: EventEntity? = null,
+ rootThreadEvent: Event? = null,
+ roomMemberContentsByUser: HashMap,
+ roomEntity: RoomEntity,
+ userId: String,
+ cryptoService: CryptoService? = null
+) {
+ when (threadSummaryType) {
+ ThreadSummaryUpdateType.REPLACE -> {
+ rootThreadEvent?.eventId ?: return
+ rootThreadEvent.senderId ?: return
+
+ val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
+
+ // Something is wrong with the server return
+ if (numberOfThreads <= 0) return
+
+ val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
+ Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
+ }
+
+ val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
+ decryptIfNeeded(cryptoService, it, roomId)
+ }
+ val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
+ decryptIfNeeded(cryptoService, it, roomId)
+ }
+ val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
+ roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
+ threadSummary.updateThreadSummary(
+ rootThreadEventEntity = rootThreadEventEntity,
+ numberOfThreads = numberOfThreads,
+ latestThreadEventEntity = latestThreadEventEntity,
+ isUserParticipating = isUserParticipating,
+ roomMemberContentsByUser = roomMemberContentsByUser
+ )
+
+ roomEntity.addIfNecessary(threadSummary)
+ }
+ ThreadSummaryUpdateType.ADD -> {
+ val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
+ Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
+
+ val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
+ if (threadSummary != null) {
+ // ThreadSummary exists so lets add the latest event
+ Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
+ threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
+ threadSummary.numberOfThreads++
+ if (threadEventEntity.sender == userId) {
+ threadSummary.isUserParticipating = true
+ }
+ } else {
+ // ThreadSummary do not exists lets try to create one
+ Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
+ threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
+ // Root thread event entity exists so lets create a new record
+ ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
+ it.updateThreadSummary(
+ rootThreadEventEntity = rootThreadEventEntity,
+ numberOfThreads = 1,
+ latestThreadEventEntity = threadEventEntity,
+ isUserParticipating = threadEventEntity.sender == userId,
+ roomMemberContentsByUser = roomMemberContentsByUser
+ )
+ roomEntity.addIfNecessary(it)
+ }
+ }
+ }
+ }
+ }
+}
+
+private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
+ cryptoService ?: return
+ val event = eventEntity.asDomain()
+ if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
+ try {
+ Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+ // 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
+ )
+ // Save decryption result, to not decrypt every time we enter the thread list
+ eventEntity.setDecryptionResult(result)
+ } catch (e: MXCryptoError) {
+ if (e is MXCryptoError.Base) {
+ event.mCryptoError = e.errorType
+ event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+ }
+ }
+ }
+}
+
+/**
+ * Request decryption
+ */
+private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
+ eventDecryptor ?: return
+ event ?: return
+ if (event.isEncrypted() &&
+ event.mxDecryptionResult == null && event.eventId != null) {
+ Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+
+ eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
+ }
+}
+
+/**
+ * If we don't have any new state on this user, get it from db
+ */
+private fun HashMap.addSenderState(realm: Realm, roomId: String, senderId: String) {
+ getOrPut(senderId) {
+ CurrentStateEventEntity
+ .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+ ?.root?.asDomain()
+ ?.getFixedRoomMemberContent()
+ }
+}
+
+/**
+ * Create an EventEntity for the root thread event or get an existing one
+ */
+private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+ val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+ return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+}
+
+/**
+ * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
+ * state
+ */
+private fun createLatestEventEntity(
+ roomId: String,
+ rootThreadEvent: Event,
+ roomMemberContentsByUser: HashMap,
+ realm: Realm): EventEntity? {
+ return getLatestEvent(rootThreadEvent)?.let {
+ it.senderId?.let { senderId ->
+ roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
+ }
+ createEventEntity(roomId, it, realm)
+ }
+}
+
+/**
+ * Returned the latest event message, if any
+ */
+private fun getLatestEvent(rootThreadEvent: Event): Event? {
+ return rootThreadEvent.unsignedData?.relations?.latestThread?.event
+}
+
+/**
+ * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
+ * note: Sorting cannot be provided by server, so we have to use that unstable property
+ * @param roomId The id of the room
+ */
+internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery =
+ ThreadSummaryEntity
+ .where(realm, roomId = roomId)
+ .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
+
+/**
+ * Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
+ */
+internal fun List.enhanceWithEditions(realm: Realm, roomId: String): List =
+ this.map {
+ it.addEditionIfNeeded(realm, roomId, true)
+ it.addEditionIfNeeded(realm, roomId, false)
+ it
+ }
+
+private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
+ val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
+ EventAnnotationsSummaryEntity
+ .where(realm, roomId, eventId)
+ .findFirst()
+ ?.editSummary
+ ?.editions
+ ?.lastOrNull()
+ ?.eventId
+ ?.let { editedEventId ->
+ TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
+ if (enhanceRoot) {
+ threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+ } else {
+ threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
index 700b94a985..069e539e2c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
@@ -19,15 +19,19 @@ package org.matrix.android.sdk.internal.database.lightweight
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
+import org.matrix.android.sdk.api.MatrixConfiguration
import javax.inject.Inject
/**
* The purpose of this class is to provide an alternative and lightweight way to store settings/data
- * on the sdi without using the database. This should be used just for sdk/user preferences and
+ * on the sdk without using the database. This should be used just for sdk/user preferences and
* not for large data sets
*/
-class LightweightSettingsStorage @Inject constructor(context: Context) {
+class LightweightSettingsStorage @Inject constructor(
+ context: Context,
+ private val matrixConfiguration: MatrixConfiguration
+) {
private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
@@ -38,7 +42,7 @@ class LightweightSettingsStorage @Inject constructor(context: Context) {
}
fun areThreadMessagesEnabled(): Boolean {
- return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
+ return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, matrixConfiguration.threadMessagesEnabledDefault)
}
companion object {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index 9c420e81fd..c3302f5ccb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -114,7 +114,7 @@ internal object EventMapper {
)
},
threadNotificationState = eventEntity.threadNotificationState,
- threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
+ threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(),
lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 7869506015..2e33988a22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
- roomVersions = mapRoomVersion(entity.roomVersionsJson)
+ roomVersions = mapRoomVersion(entity.roomVersionsJson),
+ canUseThreading = entity.canUseThreading
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
new file mode 100644
index 0000000000..cedb9e3d45
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 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.database.mapper
+
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import javax.inject.Inject
+
+internal class ThreadSummaryMapper @Inject constructor() {
+
+ fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
+ return ThreadSummary(
+ roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
+ rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
+ latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
+ rootEventId = threadSummary.rootThreadEventId.orEmpty(),
+ rootThreadSenderInfo = SenderInfo(
+ userId = threadSummary.rootThreadEventEntity?.sender ?: "",
+ displayName = threadSummary.rootThreadSenderName,
+ isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
+ avatarUrl = threadSummary.rootThreadSenderAvatar
+ ),
+ latestThreadSenderInfo = SenderInfo(
+ userId = threadSummary.latestThreadEventEntity?.sender ?: "",
+ displayName = threadSummary.latestThreadSenderName,
+ isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
+ avatarUrl = threadSummary.latestThreadSenderAvatar
+ ),
+ isUserParticipating = threadSummary.isUserParticipating,
+ numberOfThreads = threadSummary.numberOfThreads
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
index 55c7f2a8ee..e658622444 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
@@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
),
+ ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
readReceipts = readReceipts
?.distinctBy {
it.roomMember
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
new file mode 100644
index 0000000000..f108a91ecf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live thread list: using enhanced /messages api MSC3440
+ * Live thread timeline: using /relations api
+ */
+class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("ChunkEntity")
+ ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+ ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+ realm.schema.get("TimelineEventEntity")
+ ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+
+ val eventEntity = realm.schema.get("EventEntity") ?: return
+ val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
+ .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+ .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
+ .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
+ .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+ .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
+ .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
+ .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+ .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
+ .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
+ .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
+ .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
+
+ realm.schema.get("RoomEntity")
+ ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
+
+ realm.schema.get("HomeServerCapabilitiesEntity")
+ ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+ ?.forceRefreshOfHomeServerCapabilities()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index c45c27ed08..88eb821aa9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -33,7 +33,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
var timelineEvents: RealmList = RealmList(),
// Only one chunk will have isLastForward == true
@Index var isLastForward: Boolean = false,
- @Index var isLastBackward: Boolean = false
+ @Index var isLastBackward: Boolean = false,
+ // Threads
+ @Index var rootThreadEventId: String? = null,
+ @Index var isLastForwardThread: Boolean = false,
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"
@@ -47,14 +50,32 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
companion object
}
-internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
+internal fun ChunkEntity.deleteOnCascade(
+ deleteStateEvents: Boolean,
+ canDeleteRoot: Boolean) {
assertIsManaged()
if (deleteStateEvents) {
stateEvents.deleteAllFromRealm()
}
timelineEvents.clearWith {
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
+ if (deleteRoot) {
+ room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
+ }
it.deleteOnCascade(deleteRoot)
}
deleteFromRealm()
}
+
+/**
+ * Delete the chunk along with the thread events that were temporarily created
+ */
+internal fun ChunkEntity.deleteAndClearThreadEvents() {
+ assertIsManaged()
+ timelineEvents
+ .filter { it.ownedByThreadChunk }
+ .forEach {
+ it.deleteOnCascade(false)
+ }
+ deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
index 445181e576..b7158ba9cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
@@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var stateKey: String? = null,
var originServerTs: Long? = null,
@Index var sender: String? = null,
- // Can contain a serialized MatrixError
+ // Can contain a serialized MatrixError
var sendStateDetails: String? = null,
var age: Long? = 0,
var unsignedData: String? = null,
var redacts: String? = null,
var decryptionResultJson: String? = null,
var ageLocalTs: Long? = null,
- // Thread related, no need to create a new Entity for performance
+ // Thread related, no need to create a new Entity for performance
@Index var isRootThread: Boolean = false,
@Index var rootThreadEventId: String? = null,
var numberOfThreads: Int = 0,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 08ecd5995e..47a83f0ed9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,
- var lastUpdatedTimestamp: Long = 0L
+ var lastUpdatedTimestamp: Long = 0L,
+ var canUseThreading: Boolean = false
) : RealmObject() {
companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
index 2997d5d7d8..4a6f6a7bf8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
@@ -20,10 +20,14 @@ import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findRootOrLatest
+import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList = RealmList(),
var sendingTimelineEvents: RealmList = RealmList(),
+ var threadSummaries: RealmList = RealmList(),
var accountData: RealmList = RealmList()
) : RealmObject() {
@@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
}
companion object
}
+internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
+ assertIsManaged()
+ threadSummaries.findRootOrLatest(eventId)?.let {
+ threadSummaries.remove(it)
+ it.deleteFromRealm()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index c090777972..d0d23dd491 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
/**
* Realm module for Session
@@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
RoomAccountDataEntity::class,
SpaceChildSummaryEntity::class,
SpaceParentSummaryEntity::class,
- UserPresenceEntity::class
+ UserPresenceEntity::class,
+ ThreadSummaryEntity::class
])
internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index 185f0e2dcc..aacd6570bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null,
var senderMembershipEventId: String? = null,
+ // ownedByThreadChunk indicates that the current TimelineEventEntity belongs
+ // to a thread chunk and is a temporarily event.
+ var ownedByThreadChunk: Boolean = false,
var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
new file mode 100644
index 0000000000..ab9d66548e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.database.model.threads
+
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Index
+import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+
+internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "",
+ var rootThreadEventEntity: EventEntity? = null,
+ var latestThreadEventEntity: EventEntity? = null,
+ var rootThreadSenderName: String? = null,
+ var latestThreadSenderName: String? = null,
+ var rootThreadSenderAvatar: String? = null,
+ var latestThreadSenderAvatar: String? = null,
+ var rootThreadIsUniqueDisplayName: Boolean = false,
+ var isUserParticipating: Boolean = false,
+ var latestThreadIsUniqueDisplayName: Boolean = false,
+ var numberOfThreads: Int = 0
+) : RealmObject() {
+
+ @LinkingObjects("threadSummaries")
+ val room: RealmResults? = null
+
+ companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
index 156a8dd767..ece46555a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
@@ -45,10 +45,22 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.findFirst()
}
-
+internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
+ return where(realm, roomId)
+ .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+ .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+ .findFirst()
+}
+internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? {
+ return where(realm, roomId)
+ .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray())
+ .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+ .findFirst()
+}
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults {
return realm.where()
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
+ .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
.findAll()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
index 14cb7e22da..6caa832110 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
@@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
this.roomId = roomId
}
// Denormalization
- TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
+ TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach {
it.annotations = obj
}
return obj
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
new file mode 100644
index 0000000000..517d43d7cf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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.database.query
+
+import io.realm.Realm
+import io.realm.RealmList
+import io.realm.RealmQuery
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery {
+ return realm.where()
+ .equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
+}
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery {
+ return where(realm, roomId)
+ .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+}
+
+internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
+ return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject().apply {
+ this.rootThreadEventId = rootThreadEventId
+ }
+}
+internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
+ return where(realm, roomId, rootThreadEventId).findFirst()
+}
+internal fun RealmList.find(rootThreadEventId: String): ThreadSummaryEntity? {
+ return this.where()
+ .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+ .findFirst()
+}
+
+internal fun RealmList.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
+ return this.where()
+ .beginGroup()
+ .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
+ .or()
+ .equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
+ .endGroup()
+ .findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 63f41ebf2c..81d5ac835f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -97,6 +97,7 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
+ not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
}
if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 10a0d1dcec..a7317506a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -26,6 +26,7 @@ internal object TimelineEventFilter {
internal object Content {
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
+ internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
index 7d004bc5c0..fedd7d05f9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
@@ -80,8 +80,8 @@ internal class WorkManagerProvider @Inject constructor(
workManager.enqueue(checkWorkerRequest)
val checkWorkerLiveState = workManager.getWorkInfoByIdLiveData(checkWorkerRequest.id)
val observer = object : Observer {
- override fun onChanged(workInfo: WorkInfo) {
- if (workInfo.state.isFinished) {
+ override fun onChanged(workInfo: WorkInfo?) {
+ if (workInfo?.state?.isFinished == true) {
checkWorkerLiveState.removeObserver(this)
if (workInfo.state == WorkInfo.State.FAILED) {
throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" +
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
index 2820b66886..62f90f563e 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.legacy.riot;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
@@ -196,6 +197,7 @@ public class LoginStorage {
/**
* Clear the stored values
*/
+ @SuppressLint("ApplySharedPref")
public void clear() {
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 1ab1042129..5aec7db66c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -21,6 +21,7 @@ internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
+ const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
// Media
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
index b988f2253c..e9cb423893 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
@@ -30,6 +30,7 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import okio.source
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
@@ -53,6 +54,7 @@ internal class FileUploader @Inject constructor(
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val context: Context,
private val temporaryFileCreator: TemporaryFileCreator,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
contentUrlResolver: ContentUrlResolver,
moshi: Moshi
) {
@@ -146,14 +148,16 @@ internal class FileUploader @Inject constructor(
.post(requestBody)
.build()
- return okHttpClient.newCall(request).awaitResponse().use { response ->
- if (!response.isSuccessful) {
- throw response.toFailure(globalErrorReceiver)
- } else {
- response.body?.source()?.let {
- responseAdapter.fromJson(it)
+ return withContext(coroutineDispatchers.io) {
+ okHttpClient.newCall(request).awaitResponse().use { response ->
+ if (!response.isSuccessful) {
+ throw response.toFailure(globalErrorReceiver)
+ } else {
+ response.body?.source()?.let {
+ responseAdapter.fromJson(it)
+ }
+ ?: throw IOException()
}
- ?: throw IOException()
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 7415b988a4..676a4f6a38 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -17,9 +17,21 @@
package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import timber.log.Timber
internal object FilterFactory {
+ fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
+ Timber.i("$userId")
+ return RoomEventFilter(
+ limit = numberOfEvents,
+// senders = listOf(userId),
+// relationSenders = userId?.let { listOf(it) },
+ relationTypes = listOf(RelationType.THREAD)
+ )
+ }
+
fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return RoomEventFilter(
limit = numberOfEvents,
@@ -58,8 +70,8 @@ internal object FilterFactory {
private fun createElementTimelineFilter(): RoomEventFilter? {
return null // RoomEventFilter().apply {
- // TODO Enable this for optimization
- // types = listOfSupportedEventTypes.toMutableList()
+ // TODO Enable this for optimization
+ // types = listOfSupportedEventTypes.toMutableList()
// }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index f498322967..634ea73480 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -52,12 +52,13 @@ data class RoomEventFilter(
* A list of relation types which must be exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types.
*/
- @Json(name = "relation_types") val relationTypes: List? = null,
+ @Json(name = "related_by_rel_types") val relationTypes: List? = null,
/**
* A list of senders of relations which must exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types.
*/
- @Json(name = "relation_senders") val relationSenders: List? = null,
+ @Json(name = "related_by_senders") val relationSenders: List? = null,
+
/**
* A list of room IDs to include. If this list is absent then all rooms are included.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index 830a58cd12..55526b41db 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -65,7 +65,13 @@ internal data class Capabilities(
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
*/
@Json(name = "m.room_versions")
- val roomVersions: RoomVersions? = null
+ val roomVersions: RoomVersions? = null,
+ /**
+ * Capability to indicate if the server supports MSC3440 Threading
+ * True if the user can use m.thread relation, false otherwise
+ */
+ @Json(name = "m.thread")
+ val threads: BooleanCapability? = null
)
@JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index e822cbdcdb..44e13d971a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -20,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -121,6 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
}
+ homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
+ getVersionResult?.doesServerSupportThreads().orFalse()
}
if (getMediaConfigResult != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
index 8b05d2ea62..8ae203c2b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
@@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
when (event.type) {
- EventType.POLL_START,
+ in EventType.POLL_START,
EventType.MESSAGE,
EventType.REDACTION,
EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
index 144ebb5404..196a8c122d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
@@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
override fun getLinkedId(url: String): String? {
return permalinkFactory.getLinkedId(url)
}
+
+ override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+ return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
index 39c1ddfdce..0aeb0467de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
@@ -21,7 +21,10 @@ import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject
@@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
?.substringBeforeLast("?")
}
+ fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+ return buildString {
+ when (type) {
+ HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN)
+ MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_BEGIN)
+ }
+ append(baseUrl(forceMatrixTo))
+ if (useClientFormat(forceMatrixTo)) {
+ append(USER_PATH)
+ }
+ when (type) {
+ HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_END)
+ MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_END)
+ }
+ }
+ }
+
/**
* Escape '/' in id, because it is used as a separator
*
@@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
private const val GROUP_PATH = "group/"
+ private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "%2\$s"
+ private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
+ private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
index caf4158657..6f99577ac2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
@@ -68,11 +68,9 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
}
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) {
- withContext(coroutineDispatchers.io) {
- val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
- setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
- userStore.updateAvatar(userId, response.contentUri)
- }
+ val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
+ setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
+ userStore.updateAvatar(userId, response.contentUri)
}
override suspend fun getAvatarUrl(userId: String): Optional {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 2d8c3e9c78..34e859e509 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineService: TimelineService,
private val threadsService: ThreadsService,
+ private val threadsLocalService: ThreadsLocalService,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
@@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
Room,
TimelineService by timelineService,
ThreadsService by threadsService,
+ ThreadsLocalService by threadsLocalService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 4a02c55db0..1bc98015aa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -109,6 +109,10 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
}
+ override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData {
+ return roomSummaryDataSource.getCountLive(queryParams)
+ }
+
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 1e0eb8b497..8bbe3a9ac6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
@@ -86,11 +87,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
// TODO Add ?
// EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
- EventType.ENCRYPTED,
- EventType.POLL_START,
- EventType.POLL_RESPONSE,
- EventType.POLL_END
- )
+ EventType.ENCRYPTED
+ ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
return allowedTypes.contains(eventType)
@@ -117,8 +115,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
?.let {
- TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
- ?.let { tet -> tet.annotations = it }
+ TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+ ?.forEach { tet -> tet.annotations = it }
}
}
@@ -156,7 +154,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
- } else if (event.getClearType() == EventType.POLL_RESPONSE) {
+ } else if (event.getClearType() in EventType.POLL_RESPONSE) {
event.getClearContent().toModel(catchError = true)?.let { pollResponseContent ->
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
@@ -177,12 +175,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleVerification(realm, event, roomId, isLocalEcho, it)
}
}
- EventType.POLL_RESPONSE -> {
+ in EventType.POLL_RESPONSE -> {
event.getClearContent().toModel(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
}
}
- EventType.POLL_END -> {
+ in EventType.POLL_END -> {
event.content.toModel(catchError = true)?.let {
handleEndPoll(realm, event, it, roomId, isLocalEcho)
}
@@ -196,6 +194,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleReaction(realm, event, roomId, isLocalEcho)
}
}
+ // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
+// else if (event.unsignedData?.relations?.annotations != null) {
+// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
+// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
+// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
+// ?.let {
+// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+// ?.forEach { tet -> tet.annotations = it }
+// }
+// }
}
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@@ -217,7 +225,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
}
- EventType.POLL_START -> {
+ in EventType.POLL_START -> {
val content: MessagePollContent? = event.content.toModel()
if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
@@ -225,12 +233,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleReplace(realm, event, content, roomId, isLocalEcho)
}
}
- EventType.POLL_RESPONSE -> {
+ in EventType.POLL_RESPONSE -> {
event.content.toModel(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho)
}
}
- EventType.POLL_END -> {
+ in EventType.POLL_END -> {
event.content.toModel(catchError = true)?.let {
handleEndPoll(realm, event, it, roomId, isLocalEcho)
}
@@ -243,7 +251,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
// OPT OUT serer aggregation until API mature enough
- private val SHOULD_HANDLE_SERVER_AGREGGATION = false
+ private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
private fun handleReplace(realm: Realm,
event: Event,
@@ -335,13 +343,18 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
if (!isLocalEcho) {
- val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+ val replaceEvent = TimelineEventEntity
+ .where(realm, roomId, eventId)
+ .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+ .findFirst()
handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
}
}
/**
* Check if the edition is on the latest thread event, and update it accordingly
+ * @param editedEvent The event that will be changed
+ * @param replaceEvent The new event
*/
private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?,
@@ -407,12 +420,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return
}
- val option = content.response?.answers?.first() ?: return Unit.also {
+ val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
}
// Check if this option is in available options
- if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
+ if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
Timber.v("## POLL $targetEventId doesn't contain option $option")
return
}
@@ -469,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
roomId: String,
isLocalEcho: Boolean) {
val pollEventId = content.relatesTo?.eventId ?: return
-
val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
-
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel()
?.let { PowerLevelsHelper(it) }
+
if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
return
}
- var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
- if (existing == null) {
+ var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
+ if (existingPoll == null) {
Timber.v("## POLL creating new relation summary for $pollEventId")
- existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
+ existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
}
// we have it
- val existingPollSummary = existing.pollResponseSummary
+ val existingPollSummary = existingPoll.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
- existing.pollResponseSummary = it
+ existingPoll.pollResponseSummary = it
}
- if (existingPollSummary.closedTime != null) {
- Timber.v("## Received poll.end event for already ended poll $pollEventId")
- return
- }
-
val txId = event.unsignedData?.transactionId
+ existingPollSummary.closedTime = event.originServerTs
+
// is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId)
- return
}
-
- existingPollSummary.closedTime = event.originServerTs
}
private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 399bfbd0e4..10f75473b7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
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.RelationType
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -86,7 +87,7 @@ internal interface RoomAPI {
suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
@Query("from") from: String,
@Query("dir") dir: String,
- @Query("limit") limit: Int,
+ @Query("limit") limit: Int?,
@Query("filter") filter: String?
): PaginationResponse
@@ -218,7 +219,6 @@ internal interface RoomAPI {
/**
* Paginate relations for event based in normal topological order
- *
* @param relationType filter for this relation type
* @param eventType filter for this event type
*/
@@ -227,9 +227,24 @@ internal interface RoomAPI {
@Path("eventId") eventId: String,
@Path("relationType") relationType: String,
@Path("eventType") eventType: String,
+ @Query("from") from: String? = null,
+ @Query("to") to: String? = null,
@Query("limit") limit: Int? = null
): RelationsResponse
+ /**
+ * Paginate relations for thread events based in normal topological order
+ * @param relationType filter for this relation type
+ */
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
+ suspend fun getThreadsRelations(@Path("roomId") roomId: String,
+ @Path("eventId") eventId: String,
+ @Path("relationType") relationType: String = RelationType.THREAD,
+ @Query("from") from: String? = null,
+ @Query("to") to: String? = null,
+ @Query("limit") limit: Int? = null
+ ): RelationsResponse
+
/**
* Join the given room.
*
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 70c1ab4f42..72a3f9ab22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
+import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineServiceFactory: DefaultTimelineService.Factory,
private val threadsServiceFactory: DefaultThreadsService.Factory,
+ private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
@@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomSummaryDataSource = roomSummaryDataSource,
timelineService = timelineServiceFactory.create(roomId),
threadsService = threadsServiceFactory.create(roomId),
+ threadsLocalService = threadsLocalServiceFactory.create(roomId),
sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f831a77a5d..5e90076b8a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
@@ -294,4 +296,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
+
+ @Binds
+ abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index 84261e6ebf..c9914449c3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -112,19 +112,18 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
return params.avatarUri?.let { avatarUri ->
// First upload the image, ignoring any error
- tryOrNull {
+ tryOrNull("Failed to upload image") {
fileUploader.uploadFromUri(
uri = avatarUri,
filename = UUID.randomUUID().toString(),
mimeType = MimeTypes.Jpeg)
}
- ?.let { response ->
- Event(
- type = EventType.STATE_ROOM_AVATAR,
- stateKey = "",
- content = mapOf("url" to response.contentUri)
- )
- }
+ }?.let { response ->
+ Event(
+ type = EventType.STATE_ROOM_AVATAR,
+ stateKey = "",
+ content = mapOf("url" to response.contentUri)
+ )
}
}
@@ -180,19 +179,19 @@ internal class CreateRoomBodyBuilder @Inject constructor(
params.invite3pids.isEmpty() &&
params.invitedUserIds.isNotEmpty() &&
params.invitedUserIds.let { userIds ->
- val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
+ val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
- userIds.all { userId ->
- keys.map[userId].let { deviceMap ->
- if (deviceMap.isNullOrEmpty()) {
- // A user has no device, so do not enable encryption
- false
- } else {
- // Check that every user's device have at least one key
- deviceMap.values.all { !it.keys.isNullOrEmpty() }
+ userIds.all { userId ->
+ keys.map[userId].let { deviceMap ->
+ if (deviceMap.isNullOrEmpty()) {
+ // A user has no device, so do not enable encryption
+ false
+ } else {
+ // Check that every user's device have at least one key
+ deviceMap.values.all { !it.keys.isNullOrEmpty() }
+ }
+ }
}
}
- }
- }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index ee52fe574b..4753e12157 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -71,7 +71,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
when (typeToPrune) {
EventType.ENCRYPTED,
EventType.MESSAGE,
- EventType.POLL_START -> {
+ in EventType.POLL_START -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index d5019aea7b..ab514d31c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -34,7 +34,6 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDataSource
@@ -48,7 +47,6 @@ internal class DefaultRelationService @AssistedInject constructor(
private val eventFactory: LocalEchoEventFactory,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
- private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventDataSource: TimelineEventDataSource,
@SessionDatabase private val monarchy: Monarchy
) : RelationService {
@@ -196,10 +194,6 @@ internal class DefaultRelationService @AssistedInject constructor(
return eventSenderProcessor.postEvent(event)
}
- override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
- return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
- }
-
/**
* Saves the event in database as a local echo.
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
new file mode 100644
index 0000000000..b596f2288e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 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.room.relation.threads
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.filter.FilterFactory
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+/***
+ * This class is responsible to Fetch all the thread in the current room,
+ * To fetch all threads in a room, the /messages API is used with newly added filtering options.
+ */
+internal interface FetchThreadSummariesTask : Task {
+ data class Params(
+ val roomId: String,
+ val from: String = "",
+ val limit: Int = 500,
+ val isUserParticipating: Boolean = true
+ )
+}
+
+internal class DefaultFetchThreadSummariesTask @Inject constructor(
+ private val roomAPI: RoomAPI,
+ private val globalErrorReceiver: GlobalErrorReceiver,
+ @SessionDatabase private val monarchy: Monarchy,
+ private val cryptoService: DefaultCryptoService,
+ @UserId private val userId: String,
+) : FetchThreadSummariesTask {
+
+ override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
+ val filter = FilterFactory.createThreadsFilter(
+ numberOfEvents = params.limit,
+ userId = if (params.isUserParticipating) userId else null).toJSONString()
+
+ val response = executeRequest(
+ globalErrorReceiver,
+ canRetry = true
+ ) {
+ roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
+ }
+
+ Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
+
+ return handleResponse(response, params)
+ }
+
+ private suspend fun handleResponse(response: PaginationResponse,
+ params: FetchThreadSummariesTask.Params): Result {
+ val rootThreadList = response.events
+ monarchy.awaitTransaction { realm ->
+ val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
+
+ val roomMemberContentsByUser = HashMap()
+ for (rootThreadEvent in rootThreadList) {
+ if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
+ continue
+ }
+
+ ThreadSummaryEntity.createOrUpdate(
+ threadSummaryType = ThreadSummaryUpdateType.REPLACE,
+ realm = realm,
+ roomId = params.roomId,
+ rootThreadEvent = rootThreadEvent,
+ roomMemberContentsByUser = roomMemberContentsByUser,
+ roomEntity = roomEntity,
+ userId = userId,
+ cryptoService = cryptoService)
+ }
+ }
+ return Result.SUCCESS
+ }
+
+ enum class Result {
+ SHOULD_FETCH_MORE,
+ REACHED_END,
+ SUCCESS
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e0d501c515..a46bbe8d9f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 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.
@@ -20,14 +20,12 @@ import io.realm.Realm
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.EventType
-import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -36,8 +34,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
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.model.ReactionAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
@@ -47,16 +47,38 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
-internal interface FetchThreadTimelineTask : Task {
+/***
+ * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
+ *
+ * How it works
+ *
+ * The problem?
+ * - We cannot use the existing timeline architecture to paginate through the timeline
+ * - We want our new events to be live, so any interactions with them like reactions will continue to work. We should
+ * handle appropriately the existing events from /messages api with the new events from /relations.
+ * - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response
+ *
+ * The solution
+ * We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api
+ * We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and
+ * all reactions, edits etc will continue to work. If the events do not exists we create them
+ * and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline
+ *
+ */
+internal interface FetchThreadTimelineTask : Task {
data class Params(
val roomId: String,
- val rootThreadEventId: String
+ val rootThreadEventId: String,
+ val from: String?,
+ val limit: Int
+
)
}
@@ -69,94 +91,130 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
private val cryptoService: DefaultCryptoService
) : FetchThreadTimelineTask {
- override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
- val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
+ enum class Result {
+ SHOULD_FETCH_MORE,
+ REACHED_END,
+ SUCCESS
+ }
+
+ override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
val response = executeRequest(globalErrorReceiver) {
- roomAPI.getRelations(
+ roomAPI.getThreadsRelations(
roomId = params.roomId,
eventId = params.rootThreadEventId,
- relationType = RelationType.IO_THREAD,
- eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
- limit = 2000
+ from = params.from,
+ limit = params.limit
)
}
- val threadList = response.chunks + listOfNotNull(response.originalEvent)
+ Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ")
+ return handleRelationsResponse(response, params)
+ }
- return storeNewEventsIfNeeded(threadList, params.roomId)
+ private suspend fun handleRelationsResponse(response: RelationsResponse,
+ params: FetchThreadTimelineTask.Params): Result {
+ val threadList = response.chunks
+ val threadRootEvent = response.originalEvent
+ val hasReachEnd = response.nextBatch == null
+
+ monarchy.awaitTransaction { realm ->
+
+ val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId)
+ ?: run {
+ return@awaitTransaction
+ }
+
+ threadChunk.prevToken = response.nextBatch
+ val roomMemberContentsByUser = HashMap()
+
+ for (event in threadList) {
+ if (event.eventId == null || event.senderId == null || event.type == null) {
+ continue
+ }
+
+ if (threadChunk.timelineEvents.find(event.eventId) != null) {
+ // Event already exists in thread chunk, skip it
+ Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it")
+ continue
+ }
+
+ val timelineEvent = TimelineEventEntity
+ .where(realm, roomId = params.roomId, event.eventId)
+ .findFirst()
+
+ if (timelineEvent != null) {
+ // Event already exists but not in the thread chunk
+ // Lets added there
+ Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end")
+ threadChunk.timelineEvents.add(timelineEvent)
+ } else {
+ Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!")
+ val eventEntity = createEventEntity(params.roomId, event, realm)
+ roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId)
+ threadChunk.addTimelineEvent(
+ roomId = params.roomId,
+ eventEntity = eventEntity,
+ direction = PaginationDirection.FORWARDS,
+ ownedByThreadChunk = true,
+ roomMemberContentsByUser = roomMemberContentsByUser)
+ }
+ }
+
+ if (hasReachEnd) {
+ val rootThread = TimelineEventEntity
+ .where(realm, roomId = params.roomId, params.rootThreadEventId)
+ .findFirst()
+ if (rootThread != null) {
+ // If root thread event already exists add it to our chunk
+ threadChunk.timelineEvents.add(rootThread)
+ Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!")
+ } else if (threadRootEvent?.senderId != null) {
+ // Case when thread event is not in the device
+ Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one")
+ val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm)
+ roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId)
+ threadChunk.addTimelineEvent(
+ roomId = params.roomId,
+ eventEntity = eventEntity,
+ direction = PaginationDirection.FORWARDS,
+ ownedByThreadChunk = true,
+ roomMemberContentsByUser = roomMemberContentsByUser)
+ }
+ }
+ }
+
+ return if (hasReachEnd) {
+ Result.REACHED_END
+ } else {
+ Result.SHOULD_FETCH_MORE
+ }
+ }
+
+ // TODO Reuse this function to all the app
+ /**
+ * If we don't have any new state on this user, get it from db
+ */
+ private fun HashMap.addSenderState(realm: Realm, roomId: String, senderId: String) {
+ getOrPut(senderId) {
+ CurrentStateEventEntity
+ .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+ ?.root?.asDomain()
+ ?.getFixedRoomMemberContent()
+ }
}
/**
- * Store new events if they are not already received, and returns weather or not,
- * a timeline update should be made
- * @param threadList is the list containing the thread replies
- * @param roomId the roomId of the the thread
- * @return
+ * Create an EventEntity to be added in the TimelineEventEntity
*/
- private suspend fun storeNewEventsIfNeeded(threadList: List, roomId: String): Boolean {
- var eventsSkipped = 0
- monarchy
- .awaitTransaction { realm ->
- val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-
- val optimizedThreadSummaryMap = hashMapOf()
- val roomMemberContentsByUser = HashMap()
-
- for (event in threadList.reversed()) {
- if (event.eventId == null || event.senderId == null || event.type == null) {
- eventsSkipped++
- continue
- }
-
- if (EventEntity.where(realm, event.eventId).findFirst() != null) {
- // Skip if event already exists
- eventsSkipped++
- continue
- }
- if (event.isEncrypted()) {
- // Decrypt events that will be stored
- decryptIfNeeded(event, roomId)
- }
-
- handleReaction(realm, event, roomId)
-
- val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
- val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
-
- // Sender info
- roomMemberContentsByUser.getOrPut(event.senderId) {
- // If we don't have any new state on this user, get it from db
- val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
- rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
- }
-
- chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
- eventEntity.rootThreadEventId?.let {
- // This is a thread event
- optimizedThreadSummaryMap[it] = eventEntity
- } ?: run {
- // This is a normal event or a root thread one
- optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
- }
- }
-
- optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
- roomId = roomId,
- realm = realm,
- currentUserId = userId,
- shouldUpdateNotifications = false
- )
- }
- Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
-
- return eventsSkipped == threadList.size
+ private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+ val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+ return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
}
/**
* Invoke the event decryption mechanism for a specific event
*/
-
- private fun decryptIfNeeded(event: Event, roomId: String) {
+ private suspend fun decryptIfNeeded(event: Event, roomId: String) {
try {
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 28c17f38b6..31c7254ed5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -128,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
- return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
+ override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+ return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 3c36d58710..0ba95cc1fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor(
options: List,
pollType: PollType): MessagePollContent {
return MessagePollContent(
- pollCreationInfo = PollCreationInfo(
- question = PollQuestion(
- question = question
- ),
+ unstablePollCreationInfo = PollCreationInfo(
+ question = PollQuestion(unstableQuestion = question),
kind = pollType,
answers = options.map { option ->
- PollAnswer(
- id = UUID.randomUUID().toString(),
- answer = option
- )
+ PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
}
)
)
@@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
- type = EventType.POLL_START,
+ type = EventType.POLL_START.first(),
content = newContent.toContent()
)
}
@@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor(
body = answerId,
relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE,
- eventId = pollEventId),
- response = PollResponse(
- answers = listOf(answerId)
- )
-
+ eventId = pollEventId
+ ),
+ unstableResponse = PollResponse(answers = listOf(answerId))
)
val localId = LocalEcho.createLocalEchoId()
return Event(
@@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
- type = EventType.POLL_RESPONSE,
+ type = EventType.POLL_RESPONSE.first(),
content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId))
}
@@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
- type = EventType.POLL_START,
+ type = EventType.POLL_START.first(),
content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId))
}
@@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
- type = EventType.POLL_END,
+ type = EventType.POLL_END.first(),
content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId))
}
@@ -234,20 +227,17 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createLocationEvent(roomId: String,
latitude: Double,
longitude: Double,
- uncertainty: Double?): Event {
+ uncertainty: Double?,
+ isUserLocation: Boolean): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty)
+ val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
val content = MessageLocationContent(
geoUri = geoUri,
body = geoUri,
- locationInfo = LocationInfo(
- geoUri = geoUri,
- description = geoUri
- ),
- locationAsset = LocationAsset(
- type = LocationAssetType.SELF
- ),
- ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- text = geoUri
+ unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
+ unstableLocationAsset = LocationAsset(type = assetType),
+ unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+ unstableText = geoUri
)
return createMessageEvent(roomId, content)
}
@@ -353,8 +343,9 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = it,
+ isFallingBack = true,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
@@ -396,8 +387,9 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = it,
+ isFallingBack = true,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
@@ -426,8 +418,9 @@ internal class LocalEchoEventFactory @Inject constructor(
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = it,
+ isFallingBack = true,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
@@ -446,8 +439,9 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = it,
+ isFallingBack = true,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
@@ -479,7 +473,7 @@ internal class LocalEchoEventFactory @Inject constructor(
private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
var newContent: Content? = null
if (type == EventType.STICKER) {
- val isThread = (content.toModel())?.relatesTo?.type == RelationType.IO_THREAD
+ val isThread = (content.toModel())?.relatesTo?.type == RelationType.THREAD
val rootThreadEventId = (content.toModel())?.relatesTo?.eventId
if (isThread && rootThreadEventId != null) {
val newRelationalDefaultContent = (content.toModel())?.relatesTo?.copy(
@@ -560,7 +554,7 @@ internal class LocalEchoEventFactory @Inject constructor(
relatesTo = generateReplyRelationContent(
eventId = eventId,
rootThreadEventId = rootThreadEventId,
- showAsReply = showInThread))
+ showInThread = showInThread))
return createMessageEvent(roomId, content)
}
@@ -570,18 +564,20 @@ internal class LocalEchoEventFactory @Inject constructor(
* "m.relates_to": {
* "rel_type": "m.thread",
* "event_id": "$thread_root",
+ * "is_falling_back": false,
* "m.in_reply_to": {
- * "event_id": "$event_target",
- * "render_in": ["m.thread"]
- * }
- * }
+ * "event_id": "$event_target"
+ * }
+ * }
*/
- private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
+ private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
rootThreadEventId?.let {
RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = it,
- inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
+ isFallingBack = showInThread,
+ // False when is a rich reply from within a thread, and true when is a reply that should be visible from threads
+ inReplyTo = ReplyToContent(eventId = eventId))
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
@@ -638,7 +634,9 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
- MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
+ MessageType.MSGTYPE_POLL_START -> {
+ return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
+ }
else -> return TextContent(content?.body ?: "")
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
index 5c629f87f0..93c0167abe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
@@ -58,8 +58,9 @@ fun TextContent.toThreadTextContent(
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
body = text,
relatesTo = RelationDefaultContent(
- type = RelationType.IO_THREAD,
+ type = RelationType.THREAD,
eventId = rootThreadEventId,
+ isFallingBack = true,
inReplyTo = ReplyToContent(
eventId = latestThreadEventId
)),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index ccbfbfcded..fa2e0052ab 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
@@ -28,7 +29,8 @@ import javax.inject.Inject
*/
internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator,
- private val displayNameResolver: DisplayNameResolver
+ private val displayNameResolver: DisplayNameResolver,
+ private val permalinkService: PermalinkService
) {
/**
@@ -36,7 +38,7 @@ internal class TextPillsUtils @Inject constructor(
* @return the transformed String or null if no Span found
*/
fun processSpecialSpansToHtml(text: CharSequence): String? {
- return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
+ return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.HTML))
}
/**
@@ -44,7 +46,7 @@ internal class TextPillsUtils @Inject constructor(
* @return the transformed String or null if no Span found
*/
fun processSpecialSpansToMarkdown(text: CharSequence): String? {
- return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
+ return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.MARKDOWN))
}
private fun transformPills(text: CharSequence, template: String): String? {
@@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
i++
}
}
-
- companion object {
- private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s"
-
- private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
- }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index c9fc3c9575..18a4f80547 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -26,6 +26,7 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.isNormalized
@@ -42,6 +43,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
@@ -55,8 +57,10 @@ import javax.inject.Inject
internal class RoomSummaryDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
+ private val realmSessionProvider: RealmSessionProvider,
private val roomSummaryMapper: RoomSummaryMapper,
- private val queryStringValueProcessor: QueryStringValueProcessor
+ private val queryStringValueProcessor: QueryStringValueProcessor,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers
) {
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
@@ -219,14 +223,25 @@ internal class RoomSummaryDataSource @Inject constructor(
return object : UpdatableLivePageResult {
override val livePagedList: LiveData> = mapped
- override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
- realmDataSourceFactory.updateQuery {
- roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
- }
- }
-
override val liveBoundaries: LiveData
get() = boundaries
+
+ override var queryParams: RoomSummaryQueryParams = queryParams
+ set(value) {
+ field = value
+ realmDataSourceFactory.updateQuery {
+ roomSummariesQuery(it, value).process(sortOrder)
+ }
+ }
+ }
+ }
+
+ fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData {
+ val liveRooms = monarchy.findAllManagedWithChanges {
+ roomSummariesQuery(it, queryParams)
+ }
+ return Transformations.map(liveRooms) {
+ it.realmResults.where().count().toInt()
}
}
@@ -293,6 +308,7 @@ internal class RoomSummaryDataSource @Inject constructor(
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
RoomCategoryFilter.ALL -> Unit // nop
+ null -> Unit
}
// Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index 1c1d59fb3d..c9712c5721 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary
import io.realm.Realm
import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -165,7 +166,9 @@ internal class RoomSummaryUpdater @Inject constructor(
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
// mmm i want to decrypt now or is it ok to do it async?
tryOrNull {
- eventDecryptor.decryptEvent(root.asDomain(), "")
+ runBlocking {
+ eventDecryptor.decryptEvent(root.asDomain(), "")
+ }
}
?.let { root.setDecryptionResult(it) }
}
@@ -411,8 +414,6 @@ internal class RoomSummaryUpdater @Inject constructor(
realm.where(RoomSummaryEntity::class.java)
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
- // also we do not count DM in here, because home space will already show them
- .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
.findAll().forEach {
highlightCount += it.highlightCount
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
index 5967ae8d2e..b65991347d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
@@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
-import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
-import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
internal class DefaultThreadsService @AssistedInject constructor(
@Assisted private val roomId: String,
@UserId private val userId: String,
+ private val fetchThreadTimelineTask: FetchThreadTimelineTask,
+ private val fetchThreadSummariesTask: FetchThreadSummariesTask,
@SessionDatabase private val monarchy: Monarchy,
private val timelineEventMapper: TimelineEventMapper,
+ private val threadSummaryMapper: ThreadSummaryMapper
) : ThreadsService {
@AssistedFactory
@@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
fun create(roomId: String): DefaultThreadsService
}
- override fun getMarkedThreadNotificationsLive(): LiveData> {
+ override fun getAllThreadSummariesLive(): LiveData> {
return monarchy.findAllMappedWithChanges(
- { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
- { timelineEventMapper.map(it) }
+ { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+ {
+ threadSummaryMapper.map(it)
+ }
)
}
- override fun getMarkedThreadNotifications(): List {
+ override fun getAllThreadSummaries(): List {
return monarchy.fetchAllMappedSync(
- { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
- { timelineEventMapper.map(it) }
+ { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+ { threadSummaryMapper.map(it) }
)
}
- override fun getAllThreadsLive(): LiveData> {
- return monarchy.findAllMappedWithChanges(
- { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
- { timelineEventMapper.map(it) }
- )
- }
-
- override fun getAllThreads(): List {
- return monarchy.fetchAllMappedSync(
- { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
- { timelineEventMapper.map(it) }
- )
- }
-
- override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+ override fun enhanceThreadWithEditions(threads: List): List {
return Realm.getInstance(monarchy.realmConfiguration).use {
- TimelineEventEntity.isUserParticipatingInThread(
- realm = it,
- roomId = roomId,
- rootThreadEventId = rootThreadEventId,
- senderId = userId)
+ threads.enhanceWithEditions(it, roomId)
}
}
- override fun mapEventsWithEdition(threads: List): List {
- return Realm.getInstance(monarchy.realmConfiguration).use {
- threads.mapEventsWithEdition(it, roomId)
- }
+ override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
+ fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+ roomId = roomId,
+ rootThreadEventId = rootThreadEventId,
+ from = from,
+ limit = limit
+ ))
}
- override suspend fun markThreadAsRead(rootThreadEventId: String) {
- monarchy.awaitTransaction {
- EventEntity.where(
- realm = it,
- eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
- }
+ override suspend fun fetchThreadSummaries() {
+ fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
+ roomId = roomId
+ ))
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
new file mode 100644
index 0000000000..3bc36fb2a8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 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.room.threads.local
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
+import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
+import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
+import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.util.awaitTransaction
+
+internal class DefaultThreadsLocalService @AssistedInject constructor(
+ @Assisted private val roomId: String,
+ @UserId private val userId: String,
+ @SessionDatabase private val monarchy: Monarchy,
+ private val timelineEventMapper: TimelineEventMapper,
+) : ThreadsLocalService {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(roomId: String): DefaultThreadsLocalService
+ }
+
+ override fun getMarkedThreadNotificationsLive(): LiveData> {
+ return monarchy.findAllMappedWithChanges(
+ { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+ { timelineEventMapper.map(it) }
+ )
+ }
+
+ override fun getMarkedThreadNotifications(): List {
+ return monarchy.fetchAllMappedSync(
+ { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+ { timelineEventMapper.map(it) }
+ )
+ }
+
+ override fun getAllThreadsLive(): LiveData> {
+ return monarchy.findAllMappedWithChanges(
+ { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+ { timelineEventMapper.map(it) }
+ )
+ }
+
+ override fun getAllThreads(): List {
+ return monarchy.fetchAllMappedSync(
+ { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+ { timelineEventMapper.map(it) }
+ )
+ }
+
+ override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+ return Realm.getInstance(monarchy.realmConfiguration).use {
+ TimelineEventEntity.isUserParticipatingInThread(
+ realm = it,
+ roomId = roomId,
+ rootThreadEventId = rootThreadEventId,
+ senderId = userId)
+ }
+ }
+
+ override fun mapEventsWithEdition(threads: List): List {
+ return Realm.getInstance(monarchy.realmConfiguration).use {
+ threads.mapEventsWithEdition(it, roomId)
+ }
+ }
+
+ override suspend fun markThreadAsRead(rootThreadEventId: String) {
+ monarchy.awaitTransaction {
+ EventEntity.where(
+ realm = it,
+ eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 3dd4225b2c..8c2b4d2bbe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
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.SemaphoreCoroutineSequencer
@@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String,
paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask,
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+ fetchThreadTimelineTask: FetchThreadTimelineTask,
timelineEventMapper: TimelineEventMapper,
timelineInput: TimelineInput,
threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String,
realm = backgroundRealm,
eventDecryptor = eventDecryptor,
paginationTask = paginationTask,
+ realmConfiguration = realmConfiguration,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+ fetchThreadTimelineTask = fetchThreadTimelineTask,
getContextOfEventTask = getEventTask,
timelineInput = timelineInput,
timelineEventMapper = timelineEventMapper,
@@ -297,7 +301,13 @@ internal class DefaultTimeline(private val roomId: String,
Timber.v("Post snapshot of ${snapshot.size} events")
withContext(coroutineDispatchers.main) {
listeners.forEach {
- tryOrNull { it.onTimelineUpdated(snapshot) }
+ if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) {
+ // We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found
+ tryOrNull { it.onTimelineUpdated(arrayListOf()) }
+ } else {
+ // In all the other cases update timeline as expected
+ tryOrNull { it.onTimelineUpdated(snapshot) }
+ }
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index 8094fee504..1ba2aff191 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
@@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
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.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
@@ -42,6 +43,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+ private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventMapper: TimelineEventMapper,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -64,10 +66,11 @@ internal class DefaultTimelineService @AssistedInject constructor(
realmConfiguration = monarchy.realmConfiguration,
coroutineDispatchers = coroutineDispatchers,
paginationTask = paginationTask,
+ fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
timelineInput = timelineInput,
eventDecryptor = eventDecryptor,
- fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+ fetchThreadTimelineTask = fetchThreadTimelineTask,
loadRoomMembersTask = loadRoomMembersTask,
readReceiptHandler = readReceiptHandler,
getEventTask = contextOfEventTask,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index f332c4a35f..a9e7b3bcdc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -19,20 +19,28 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
+import io.realm.RealmConfiguration
import io.realm.RealmResults
+import io.realm.kotlin.createObject
import kotlinx.coroutines.CompletableDeferred
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
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.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
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.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
+import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
/**
@@ -76,6 +84,8 @@ internal class LoadTimelineStrategy(
val realm: AtomicReference,
val eventDecryptor: TimelineEventDecryptor,
val paginationTask: PaginationTask,
+ val realmConfiguration: RealmConfiguration,
+ val fetchThreadTimelineTask: FetchThreadTimelineTask,
val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
val getContextOfEventTask: GetContextOfEventTask,
val timelineInput: TimelineInput,
@@ -90,7 +100,6 @@ internal class LoadTimelineStrategy(
private var getContextLatch: CompletableDeferred? = null
private var chunkEntity: RealmResults? = null
private var timelineChunk: TimelineChunk? = null
-
private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults, changeSet: OrderedCollectionChangeSet ->
// Can be call either when you open a permalink on an unknown event
// or when there is a gap in the timeline.
@@ -170,6 +179,9 @@ internal class LoadTimelineStrategy(
getContextLatch?.cancel()
chunkEntity = null
timelineChunk = null
+ if (mode is Mode.Thread) {
+ clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
+ }
}
suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@@ -185,6 +197,9 @@ internal class LoadTimelineStrategy(
return LoadMoreResult.FAILURE
}
}
+ if (mode is Mode.Thread) {
+ return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE
+ }
return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
}
@@ -201,7 +216,7 @@ internal class LoadTimelineStrategy(
}
private fun buildSendingEvents(): List {
- return if (hasReachedLastForward()) {
+ return if (hasReachedLastForward() || mode is Mode.Thread) {
sendingEventsDataSource.buildSendingEvents()
} else {
emptyList()
@@ -219,13 +234,47 @@ internal class LoadTimelineStrategy(
ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
}
is Mode.Thread -> {
+ recreateThreadChunkEntity(realm, mode.rootThreadEventId)
ChunkEntity.where(realm, roomId)
- .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+ .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId)
+ .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
.findAll()
}
}
}
+ /**
+ * Clear any existing thread chunk entity and create a new one, with the
+ * rootThreadEventId included
+ */
+ private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+ realm.executeTransaction {
+ // Lets delete the chunk and start a new one
+ ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+ Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
+ }
+ val threadChunk = it.createObject().apply {
+ Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId")
+ this.rootThreadEventId = rootThreadEventId
+ this.isLastForwardThread = true
+ }
+ if (threadChunk.isValid) {
+ RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk)
+ }
+ }
+ }
+
+ /**
+ * Clear any existing thread chunk
+ */
+ private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+ realm.executeTransaction {
+ ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+ Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
+ }
+ }
+ }
+
private fun hasReachedLastForward(): Boolean {
return timelineChunk?.hasReachedLastForward().orFalse()
}
@@ -237,8 +286,10 @@ internal class LoadTimelineStrategy(
timelineSettings = dependencies.timelineSettings,
roomId = roomId,
timelineId = timelineId,
+ fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
eventDecryptor = dependencies.eventDecryptor,
paginationTask = dependencies.paginationTask,
+ realmConfiguration = dependencies.realmConfiguration,
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
timelineEventMapper = dependencies.timelineEventMapper,
uiEchoManager = uiEchoManager,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index b7a2cf2fce..cb61222de7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -55,6 +55,7 @@ internal class RealmSendingEventsDataSource(
roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
sendingTimelineEvents = roomEntity?.sendingTimelineEvents
sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
+ updateFrozenResults(sendingTimelineEvents)
}
override fun stop() {
@@ -66,7 +67,9 @@ internal class RealmSendingEventsDataSource(
private fun updateFrozenResults(sendingEvents: RealmList?) {
// Makes sure to close the previous frozen realm
- frozenSendingTimelineEvents?.realm?.close()
+ if (frozenSendingTimelineEvents?.isValid == true) {
+ frozenSendingTimelineEvents?.realm?.close()
+ }
frozenSendingTimelineEvents = sendingEvents?.freeze()
}
@@ -74,13 +77,15 @@ internal class RealmSendingEventsDataSource(
val builtSendingEvents = mutableListOf()
uiEchoManager.getInMemorySendingEvents()
.addWithUiEcho(builtSendingEvents)
- frozenSendingTimelineEvents
- ?.filter { timelineEvent ->
- builtSendingEvents.none { it.eventId == timelineEvent.eventId }
- }
- ?.map {
- timelineEventMapper.map(it)
- }?.addWithUiEcho(builtSendingEvents)
+ if (frozenSendingTimelineEvents?.isValid == true) {
+ frozenSendingTimelineEvents
+ ?.filter { timelineEvent ->
+ builtSendingEvents.none { it.eventId == timelineEvent.eventId }
+ }
+ ?.map {
+ timelineEventMapper.map(it)
+ }?.addWithUiEcho(builtSendingEvents)
+ }
return builtSendingEvents
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index c0dc31fcf8..c8f2132ae6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.RealmConfiguration
import io.realm.RealmObjectChangeListener
import io.realm.RealmQuery
import io.realm.RealmResults
@@ -36,6 +37,8 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import timber.log.Timber
import java.util.Collections
@@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
private val timelineSettings: TimelineSettings,
private val roomId: String,
private val timelineId: String,
+ private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
+ private val realmConfiguration: RealmConfiguration,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val uiEchoManager: UIEchoManager? = null,
@@ -78,11 +83,15 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
isLastBackward.set(chunkEntity.isLastBackward)
}
if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
- nextChunk = createTimelineChunk(chunkEntity.nextChunk)
+ nextChunk = createTimelineChunk(chunkEntity.nextChunk).also {
+ it?.prevChunk = this
+ }
nextChunkLatch?.complete(Unit)
}
if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
- prevChunk = createTimelineChunk(chunkEntity.prevChunk)
+ prevChunk = createTimelineChunk(chunkEntity.prevChunk).also {
+ it?.nextChunk = this
+ }
prevChunkLatch?.complete(Unit)
}
}
@@ -141,29 +150,57 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
val loadFromStorage = loadFromStorage(count, direction).also {
logLoadedFromStorage(it, direction)
}
+ if (loadFromStorage.numberOfEvents == 6) {
+ Timber.i("here")
+ }
val offsetCount = count - loadFromStorage.numberOfEvents
- return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
+ return if (offsetCount == 0) {
+ LoadMoreResult.SUCCESS
+ } else if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
LoadMoreResult.REACHED_END
} else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
LoadMoreResult.REACHED_END
} else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
LoadMoreResult.REACHED_END
- } else if (offsetCount == 0) {
- LoadMoreResult.SUCCESS
} else {
delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
}
}
+ /**
+ * This function will fetch more live thread timeline events using the /relations api. It will
+ * always fetch results, while we want our data to be up to dated.
+ */
+ suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult {
+ val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
+ return if (direction == Timeline.Direction.BACKWARDS) {
+ try {
+ fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+ roomId,
+ rootThreadEventId,
+ chunkEntity.prevToken,
+ count
+ )).toLoadMoreResult()
+ } catch (failure: Throwable) {
+ Timber.e(failure, "Failed to fetch thread timeline events from the server")
+ LoadMoreResult.FAILURE
+ }
+ } else {
+ LoadMoreResult.FAILURE
+ }
+ }
+
private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
return if (direction == Timeline.Direction.FORWARDS) {
val nextChunkEntity = chunkEntity.nextChunk
when {
nextChunkEntity != null -> {
if (nextChunk == null) {
- nextChunk = createTimelineChunk(nextChunkEntity)
+ nextChunk = createTimelineChunk(nextChunkEntity).also {
+ it?.prevChunk = this
+ }
}
nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
}
@@ -179,7 +216,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
when {
prevChunkEntity != null -> {
if (prevChunk == null) {
- prevChunk = createTimelineChunk(prevChunkEntity)
+ prevChunk = createTimelineChunk(prevChunkEntity).also {
+ it?.nextChunk = this
+ }
}
prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
}
@@ -413,6 +452,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
}
}
+ private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult {
+ return when (this) {
+ DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END
+ DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE,
+ DefaultFetchThreadTimelineTask.Result.SUCCESS -> LoadMoreResult.SUCCESS
+ }
+ }
+
private fun getOffsetIndex(): Int {
var offset = 0
var currentNextChunk = nextChunk
@@ -454,6 +501,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
}
}
}
+
if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
onBuiltEvents(true)
}
@@ -487,6 +535,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
timelineId = timelineId,
eventDecryptor = eventDecryptor,
paginationTask = paginationTask,
+ realmConfiguration = realmConfiguration,
+ fetchThreadTimelineTask = fetchThreadTimelineTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper,
uiEchoManager = uiEchoManager,
@@ -508,13 +558,18 @@ private fun RealmQuery.offsets(
count: Int,
startDisplayIndex: Int
): RealmQuery {
- sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
- if (direction == Timeline.Direction.BACKWARDS) {
+ return if (direction == Timeline.Direction.BACKWARDS) {
lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+ sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+ limit(count.toLong())
} else {
greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+ // We need to sort ascending first so limit works in the right direction
+ sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+ limit(count.toLong())
+ // Result is expected to be sorted descending
+ sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
}
- return limit(count.toLong())
}
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
@@ -533,7 +588,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR
.or()
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
.endGroup()
- .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index 49a8a8b55a..3ddd877b78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.Realm
import io.realm.RealmConfiguration
+import kotlinx.coroutines.runBlocking
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
@@ -99,7 +100,13 @@ internal class TimelineEventDecryptor @Inject constructor(
}
executor?.execute {
Realm.getInstance(realmConfiguration).use { realm ->
- processDecryptRequest(request, realm)
+ try {
+ runBlocking {
+ processDecryptRequest(request, realm)
+ }
+ } catch (e: InterruptedException) {
+ Timber.i("Decryption got interrupted")
+ }
}
}
}
@@ -115,7 +122,7 @@ internal class TimelineEventDecryptor @Inject constructor(
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
}
}
- private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
+ private suspend fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
val event = request.event
val timelineId = request.timelineId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index 6607e71bd9..63383a99b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -34,6 +34,7 @@ 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.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find
@@ -49,10 +50,10 @@ import javax.inject.Inject
* Insert Chunk in DB, and eventually link next and previous chunk in db.
*/
internal class TokenChunkEventPersistor @Inject constructor(
- @SessionDatabase private val monarchy: Monarchy,
- @UserId private val userId: String,
- private val lightweightSettingsStorage: LightweightSettingsStorage,
- private val liveEventManager: Lazy) {
+ @SessionDatabase private val monarchy: Monarchy,
+ @UserId private val userId: String,
+ private val lightweightSettingsStorage: LightweightSettingsStorage,
+ private val liveEventManager: Lazy) {
enum class Result {
SHOULD_FETCH_MORE,
@@ -145,9 +146,12 @@ internal class TokenChunkEventPersistor @Inject constructor(
if (event.eventId == null || event.senderId == null) {
return@forEach
}
- // We check for the timeline event with this id
+ // We check for the timeline event with this id, but not in the thread chunk
val eventId = event.eventId
- val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+ val existingTimelineEvent = TimelineEventEntity
+ .where(realm, roomId, eventId)
+ .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+ .findFirst()
// If it exists, we want to stop here, just link the prevChunk
val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
if (existingChunk != null) {
@@ -173,7 +177,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
return@processTimelineEvents
}
val ageLocalTs = event.unsignedData?.age?.let { now - it }
- val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+ var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
event.prevContent
@@ -183,7 +187,11 @@ internal class TokenChunkEventPersistor @Inject constructor(
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel()
}
liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
- currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
+ currentChunk.addTimelineEvent(
+ roomId = roomId,
+ eventEntity = eventEntity,
+ direction = direction,
+ roomMemberContentsByUser = roomMemberContentsByUser)
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
eventEntity.rootThreadEventId?.let {
// This is a thread event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
index 7ac34e80e9..e5213c4696 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.signout
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
+import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.cleanup.CleanupSession
@@ -65,7 +66,13 @@ internal class DefaultSignOutTask @Inject constructor(
// Logout from identity server if any
runCatching { identityDisconnectTask.execute(Unit) }
- .onFailure { Timber.w(it, "Unable to disconnect identity server") }
+ .onFailure {
+ if (it is IdentityServiceError.NoIdentityServerConfigured) {
+ Timber.i("No identity server configured to disconnect")
+ } else {
+ Timber.w(it, "Unable to disconnect identity server")
+ }
+ }
Timber.d("SignOut: cleanup session...")
cleanupSession.cleanup()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
index c18055e089..e764ab551a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
@@ -113,71 +113,108 @@ internal class DefaultSpaceService @Inject constructor(
return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
}
- override suspend fun querySpaceChildren(spaceId: String,
- suggestedOnly: Boolean?,
- limit: Int?,
- from: String?,
- knownStateList: List?): SpaceHierarchyData {
- return resolveSpaceInfoTask.execute(
- ResolveSpaceInfoTask.Params(
- spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly
- )
- ).let { response ->
- val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
- val root = RoomSummary(
- roomId = spaceDesc?.roomId ?: spaceId,
- roomType = spaceDesc?.roomType,
- name = spaceDesc?.name ?: "",
- displayName = spaceDesc?.name ?: "",
- topic = spaceDesc?.topic ?: "",
- joinedMembersCount = spaceDesc?.numJoinedMembers,
- avatarUrl = spaceDesc?.avatarUrl ?: "",
- encryptionEventTs = null,
- typingUsers = emptyList(),
- isEncrypted = false,
- flattenParentIds = emptyList(),
- canonicalAlias = spaceDesc?.canonicalAlias,
- joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
- )
- val children = response.rooms
- ?.filter { it.roomId != spaceId }
- ?.flatMap { childSummary ->
- (spaceDesc?.childrenState ?: knownStateList)
- ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
- ?.mapNotNull { childStateEv ->
- // create a child entry for everytime this room is the child of a space
- // beware that a room could appear then twice in this list
- childStateEv.content.toModel()?.let { childStateEvContent ->
- SpaceChildInfo(
- childRoomId = childSummary.roomId,
- isKnown = true,
- roomType = childSummary.roomType,
- name = childSummary.name,
- topic = childSummary.topic,
- avatarUrl = childSummary.avatarUrl,
- order = childStateEvContent.order,
-// autoJoin = childStateEvContent.autoJoin ?: false,
- viaServers = childStateEvContent.via.orEmpty(),
- activeMemberCount = childSummary.numJoinedMembers,
- parentRoomId = childStateEv.roomId,
- suggested = childStateEvContent.suggested,
- canonicalAlias = childSummary.canonicalAlias,
- aliases = childSummary.aliases,
- worldReadable = childSummary.worldReadable
- )
- }
- }.orEmpty()
- }
- .orEmpty()
- SpaceHierarchyData(
- rootSummary = root,
- children = children,
- childrenState = spaceDesc?.childrenState.orEmpty(),
- nextToken = response.nextBatch
- )
- }
+ override suspend fun querySpaceChildren(
+ spaceId: String,
+ suggestedOnly: Boolean?,
+ limit: Int?,
+ from: String?,
+ knownStateList: List?
+ ): SpaceHierarchyData {
+ val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from)
+ val spaceRootResponse = spacesResponse.getRoot(spaceId)
+ val spaceRoot = spaceRootResponse?.toRoomSummary() ?: createBlankRoomSummary(spaceId)
+ val spaceChildren = spacesResponse.rooms.mapSpaceChildren(spaceId, spaceRootResponse, knownStateList)
+
+ return SpaceHierarchyData(
+ rootSummary = spaceRoot,
+ children = spaceChildren,
+ childrenState = spaceRootResponse?.childrenState.orEmpty(),
+ nextToken = spacesResponse.nextBatch
+ )
}
+ private suspend fun getSpacesResponse(spaceId: String, suggestedOnly: Boolean?, limit: Int?, from: String?) =
+ resolveSpaceInfoTask.execute(
+ ResolveSpaceInfoTask.Params(spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly)
+ )
+
+ private fun SpacesResponse.getRoot(spaceId: String) = rooms?.firstOrNull { it.roomId == spaceId }
+
+ private fun SpaceChildSummaryResponse.toRoomSummary() = RoomSummary(
+ roomId = roomId,
+ roomType = roomType,
+ name = name ?: "",
+ displayName = name ?: "",
+ topic = topic ?: "",
+ joinedMembersCount = numJoinedMembers,
+ avatarUrl = avatarUrl ?: "",
+ encryptionEventTs = null,
+ typingUsers = emptyList(),
+ isEncrypted = false,
+ flattenParentIds = emptyList(),
+ canonicalAlias = canonicalAlias,
+ joinRules = RoomJoinRules.PUBLIC.takeIf { isWorldReadable }
+ )
+
+ private fun createBlankRoomSummary(spaceId: String) = RoomSummary(
+ roomId = spaceId,
+ joinedMembersCount = null,
+ encryptionEventTs = null,
+ typingUsers = emptyList(),
+ isEncrypted = false,
+ flattenParentIds = emptyList(),
+ canonicalAlias = null,
+ joinRules = null
+ )
+
+ private fun List?.mapSpaceChildren(
+ spaceId: String,
+ spaceRootResponse: SpaceChildSummaryResponse?,
+ knownStateList: List?,
+ ) = this?.filterIdIsNot(spaceId)
+ ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList)
+ .orEmpty()
+
+ private fun List.filterIdIsNot(spaceId: String) = filter { it.roomId != spaceId }
+
+ private fun List.toSpaceChildInfoList(
+ spaceId: String,
+ rootRoomResponse: SpaceChildSummaryResponse?,
+ knownStateList: List?,
+ ) = flatMap { spaceChildSummary ->
+ (rootRoomResponse?.childrenState ?: knownStateList)
+ ?.filter { it.isChildOf(spaceChildSummary) }
+ ?.mapNotNull { childStateEvent -> childStateEvent.toSpaceChildInfo(spaceId, spaceChildSummary) }
+ .orEmpty()
+ }
+
+ private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD
+
+ private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel()?.let { content ->
+ createSpaceChildInfo(spaceId, summary, content)
+ }
+
+ private fun createSpaceChildInfo(
+ spaceId: String,
+ summary: SpaceChildSummaryResponse,
+ content: SpaceChildContent
+ ) = SpaceChildInfo(
+ childRoomId = summary.roomId,
+ isKnown = true,
+ roomType = summary.roomType,
+ name = summary.name,
+ topic = summary.topic,
+ avatarUrl = summary.avatarUrl,
+ order = content.order,
+ viaServers = content.via.orEmpty(),
+ activeMemberCount = summary.numJoinedMembers,
+ parentRoomId = spaceId,
+ suggested = content.suggested,
+ canonicalAlias = summary.canonicalAlias,
+ aliases = summary.aliases,
+ worldReadable = summary.isWorldReadable
+ )
+
override suspend fun joinSpace(spaceIdOrAlias: String,
reason: String?,
viaServers: List): JoinSpaceResult {
@@ -192,10 +229,6 @@ internal class DefaultSpaceService @Inject constructor(
leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
}
-// override fun getSpaceParentsOfRoom(roomId: String): List {
-// return spaceSummaryDataSource.getParentsOfRoom(roomId)
-// }
-
override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List) {
// Should we perform some validation here?,
// and if client want to bypass, it could use sendStateEvent directly?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
index 2a396d6ee7..d59ca06c2c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
+import retrofit2.HttpException
import javax.inject.Inject
internal interface ResolveSpaceInfoTask : Task {
@@ -28,7 +29,6 @@ internal interface ResolveSpaceInfoTask : Task
+ // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
measureTimeMillis {
Timber.v("Handle rooms")
reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index b4da1a02cd..2136259f22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync
import android.os.SystemClock
import okhttp3.ResponseBody
+import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.initsync.InitSyncStep
@@ -104,7 +105,11 @@ internal class DefaultSyncTask @Inject constructor(
val isInitialSync = token == null
if (isInitialSync) {
// We might want to get the user information in parallel too
- userStore.createOrUpdate(userId)
+ val user = tryOrNull { session.getProfileAsUser(userId) }
+ userStore.createOrUpdate(
+ userId = userId,
+ displayName = user?.displayName,
+ avatarUrl = user?.avatarUrl)
defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100)
}
// Maybe refresh the homeserver capabilities data we know
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index f299d3effa..9ae7b82777 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -38,7 +38,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
private val verificationService: DefaultVerificationService) {
- fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
+ suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
val total = toDevice.events?.size ?: 0
toDevice.events?.forEachIndexed { index, event ->
progressReporter?.reportProgress(index * 100F / total)
@@ -66,7 +66,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
* @param timelineId the timeline identifier
* @return true if the event has been decrypted
*/
- private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
+ private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
Timber.v("## CRYPTO | decryptToDeviceEvent")
if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null
@@ -80,6 +80,8 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
it.identityKey() == senderKey
}?.deviceId ?: senderKey
Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
+ } catch (failure: Throwable) {
+ Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}")
}
if (null != result) {
@@ -91,7 +93,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
)
return true
} else {
- // should not happen
+ // Could happen for to device events
+ // None of the known session could decrypt the message
+ // In this case unwedging process might have been started (rate limited)
Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
index fe173a35c3..e5bed12181 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm
+import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPresenceContent
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
@@ -27,27 +28,29 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
import org.matrix.android.sdk.internal.database.query.updateUserPresence
import javax.inject.Inject
-internal class PresenceSyncHandler @Inject constructor() {
+internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
- presenceSyncResponse?.events
- ?.filter { event -> event.type == EventType.PRESENCE }
- ?.forEach { event ->
- val content = event.getPresenceContent() ?: return@forEach
- val userId = event.senderId ?: return@forEach
- val userPresenceEntity = UserPresenceEntity(
- userId = userId,
- lastActiveAgo = content.lastActiveAgo,
- statusMessage = content.statusMessage,
- isCurrentlyActive = content.isCurrentlyActive,
- avatarUrl = content.avatarUrl,
- displayName = content.displayName
- ).also {
- it.presence = content.presence
- }
+ if (matrixConfiguration.presenceSyncEnabled) {
+ presenceSyncResponse?.events
+ ?.filter { event -> event.type == EventType.PRESENCE }
+ ?.forEach { event ->
+ val content = event.getPresenceContent() ?: return@forEach
+ val userId = event.senderId ?: return@forEach
+ val userPresenceEntity = UserPresenceEntity(
+ userId = userId,
+ lastActiveAgo = content.lastActiveAgo,
+ statusMessage = content.statusMessage,
+ isCurrentlyActive = content.isCurrentlyActive,
+ avatarUrl = content.avatarUrl,
+ displayName = content.displayName
+ ).also {
+ it.presence = content.presence
+ }
- storePresenceToDB(realm, userPresenceEntity)
- }
+ storePresenceToDB(realm, userPresenceEntity)
+ }
+ }
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 99e6521eb7..8fe85f0d31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -19,14 +19,17 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import dagger.Lazy
import io.realm.Realm
import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.initsync.InitSyncStep
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.api.session.sync.model.RoomSync
@@ -36,6 +39,7 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -46,10 +50,13 @@ 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.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
@@ -84,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
+ private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val timelineInput: TimelineInput,
private val liveEventService: Lazy) {
@@ -94,11 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
data class LEFT(val data: Map) : HandlingStrategy()
}
- fun handle(realm: Realm,
- roomsSyncResponse: RoomsSyncResponse,
- isInitialSync: Boolean,
- aggregator: SyncResponsePostTreatmentAggregator,
- reporter: ProgressReporter? = null) {
+ suspend fun handle(realm: Realm,
+ roomsSyncResponse: RoomsSyncResponse,
+ isInitialSync: Boolean,
+ aggregator: SyncResponsePostTreatmentAggregator,
+ reporter: ProgressReporter? = null) {
Timber.v("Execute transaction from $this")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
@@ -113,11 +121,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
// PRIVATE METHODS *****************************************************************************
- private fun handleRoomSync(realm: Realm,
- handlingStrategy: HandlingStrategy,
- isInitialSync: Boolean,
- aggregator: SyncResponsePostTreatmentAggregator,
- reporter: ProgressReporter?) {
+ private suspend fun handleRoomSync(realm: Realm,
+ handlingStrategy: HandlingStrategy,
+ isInitialSync: Boolean,
+ aggregator: SyncResponsePostTreatmentAggregator,
+ reporter: ProgressReporter?) {
val insertType = if (isInitialSync) {
EventInsertType.INITIAL_SYNC
} else {
@@ -150,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
realm.insertOrUpdate(rooms)
}
- private fun insertJoinRoomsFromInitSync(realm: Realm,
- handlingStrategy: HandlingStrategy.JOINED,
- syncLocalTimeStampMillis: Long,
- aggregator: SyncResponsePostTreatmentAggregator,
- reporter: ProgressReporter?) {
+ private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
+ handlingStrategy: HandlingStrategy.JOINED,
+ syncLocalTimeStampMillis: Long,
+ aggregator: SyncResponsePostTreatmentAggregator,
+ reporter: ProgressReporter?) {
val bestChunkSize = computeBestChunkSize(
listSize = handlingStrategy.data.keys.size,
limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
@@ -192,12 +200,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
}
- private fun handleJoinedRoom(realm: Realm,
- roomId: String,
- roomSync: RoomSync,
- insertType: EventInsertType,
- syncLocalTimestampMillis: Long,
- aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
+ private suspend fun handleJoinedRoom(realm: Realm,
+ roomId: String,
+ roomSync: RoomSync,
+ insertType: EventInsertType,
+ syncLocalTimestampMillis: Long,
+ aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
Timber.v("Handle join sync for room $roomId")
val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
@@ -343,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return roomEntity
}
- private fun handleTimelineEvents(realm: Realm,
- roomId: String,
- roomEntity: RoomEntity,
- eventList: List,
- prevToken: String? = null,
- isLimited: Boolean = true,
- insertType: EventInsertType,
- syncLocalTimestampMillis: Long,
- aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
+ private suspend fun handleTimelineEvents(realm: Realm,
+ roomId: String,
+ roomEntity: RoomEntity,
+ eventList: List,
+ prevToken: String? = null,
+ isLimited: Boolean = true,
+ insertType: EventInsertType,
+ syncLocalTimestampMillis: Long,
+ aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
if (isLimited && lastChunk != null) {
lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
@@ -379,7 +387,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
if (event.isEncrypted() && !isInitialSync) {
- decryptIfNeeded(event, roomId)
+ runBlocking {
+ decryptIfNeeded(event, roomId)
+ }
}
var contentToInject: String? = null
if (!isInitialSync) {
@@ -406,11 +416,28 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
}
- chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+ val timelineEventAdded = chunkEntity.addTimelineEvent(
+ roomId = roomId,
+ eventEntity = eventEntity,
+ direction = PaginationDirection.FORWARDS,
+ roomMemberContentsByUser = roomMemberContentsByUser)
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
eventEntity.rootThreadEventId?.let {
// This is a thread event
optimizedThreadSummaryMap[it] = eventEntity
+ // Add the same thread timeline event to Thread Chunk
+ addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
+ if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+ // Update thread summaries only if homeserver supports threading
+ ThreadSummaryEntity.createOrUpdate(
+ threadSummaryType = ThreadSummaryUpdateType.ADD,
+ realm = realm,
+ roomId = roomId,
+ threadEventEntity = eventEntity,
+ roomMemberContentsByUser = roomMemberContentsByUser,
+ userId = userId,
+ roomEntity = roomEntity)
+ }
} ?: run {
// This is a normal event or a root thread one
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
@@ -455,7 +482,29 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return chunkEntity
}
- private fun decryptIfNeeded(event: Event, roomId: String) {
+ /**
+ * Adds new event to the appropriate thread chunk. If the event is already in
+ * the thread timeline and /relations api, we should not added it
+ */
+ private fun addToThreadChunkIfNeeded(realm: Realm,
+ roomId: String,
+ threadId: String,
+ timelineEventEntity: TimelineEventEntity?,
+ roomEntity: RoomEntity) {
+ val eventId = timelineEventEntity?.eventId ?: return
+
+ ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->
+ val existingEvent = threadChunk.timelineEvents.find(eventId)
+ if (existingEvent?.ownedByThreadChunk == true) {
+ Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add")
+ return@addToThreadChunkIfNeeded
+ }
+ threadChunk.timelineEvents.add(0, timelineEventEntity)
+ roomEntity.addIfNecessary(threadChunk)
+ }
+ }
+
+ private suspend fun decryptIfNeeded(event: Event, roomId: String) {
try {
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index f3a1523955..db9799d51e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -32,6 +32,7 @@ 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.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
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
@@ -161,7 +162,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
eventEntity: EventEntity? = null): String? {
event ?: return null
roomId ?: return null
- if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
+ if (lightweightSettingsStorage.areThreadMessagesEnabled() && !isReplyEvent(event)) return null
handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
if (!isThreadEvent(event)) return null
val eventPayload = if (!event.isEncrypted()) {
@@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
}
val eventBody = event.getDecryptedTextSummary() ?: return null
+ val threadRelation = getRootThreadRelationContent(event)
val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
- return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+ return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
}
val eventToInject = getEventFromDB(realm, eventIdToInject)
val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
@@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId,
eventBody = eventBody,
eventToInject = eventToInject,
- eventToInjectBody = eventToInjectBody) ?: return null
+ eventToInjectBody = eventToInjectBody,
+ threadRelation = threadRelation) ?: return null
+
// update the event
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
} else {
- contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+ contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
}
// Now lets try to find relations for improved results, while some events may come with reverse order
eventEntity?.let {
// When eventEntity is not null means that we are not from within roomSyncHandler
- handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
+ handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation)
}
return contentForNonEncrypted
}
@@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param event the current event received
* @return The content to inject in the roomSyncHandler live events
*/
- private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? {
+ private fun handleRootThreadEventsIfNeeded(
+ realm: Realm,
+ roomId: String,
+ eventEntity: EventEntity?,
+ event: Event
+ ): String? {
if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
eventEntity?.let {
val eventBody = event.getDecryptedTextSummary() ?: return null
- return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
+ return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null)
}
}
return null
@@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param isFromCache determines whether or not we already know this is root thread event
* @return The content to inject in the roomSyncHandler live events
*/
- private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
+ private fun handleEventsThatRelatesTo(
+ realm: Realm,
+ roomId: String,
+ event: Event,
+ eventBody: String,
+ isFromCache: Boolean,
+ threadRelation: RelationDefaultContent?
+ ): String? {
event.eventId ?: return null
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
@@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId,
eventBody = newEventBody,
eventToInject = event,
- eventToInjectBody = eventBody) ?: return null
+ eventToInjectBody = eventBody,
+ threadRelation = threadRelation) ?: return null
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
}
@@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectEvent(roomId: String,
eventBody: String,
eventToInject: Event,
- eventToInjectBody: String): Content? {
+ eventToInjectBody: String,
+ threadRelation: RelationDefaultContent?
+ ): Content? {
val eventToInjectId = eventToInject.eventId ?: return null
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
@@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
eventBody)
return MessageTextContent(
+ relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody,
@@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectFallbackIndicator(event: Event,
eventBody: String,
eventEntity: EventEntity?,
- eventPayload: MutableMap): String? {
+ eventPayload: MutableMap,
+ threadRelation: RelationDefaultContent?): String? {
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
"In reply to a thread",
eventBody)
val messageTextContent = MessageTextContent(
+ relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody,
@@ -332,7 +354,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
.findAll()
cacheEventRootId.add(rootThreadEventId)
return threadList.filter {
- it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
+ it.asDomain().getRelationContentForType(RelationType.THREAD)?.inReplyTo?.eventId == currentEventId
}
}
@@ -350,7 +372,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param event
*/
private fun isThreadEvent(event: Event): Boolean =
- event.content.toModel()?.relatesTo?.type == RelationType.IO_THREAD
+ event.content.toModel()?.relatesTo?.type == RelationType.THREAD
/**
* Returns the root thread eventId or null otherwise
@@ -359,9 +381,22 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun getRootThreadEventId(event: Event): String? =
event.content.toModel()?.relatesTo?.eventId
+ private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
+ event.content.toModel()?.relatesTo
+
private fun getPreviousEventOrRoot(event: Event): String? =
event.content.toModel()?.relatesTo?.inReplyTo?.eventId
+ /**
+ * Returns if we should html inject the current event.
+ */
+ private fun isReplyEvent(event: Event): Boolean {
+ return isThreadEvent(event) && !isFallingBack(event) && getPreviousEventOrRoot(event) != null
+ }
+
+ private fun isFallingBack(event: Event): Boolean =
+ event.content.toModel()?.relatesTo?.isFallingBack == true
+
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
val content = payload?.get("content") as? JsonDict
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
index 423a4e553f..c67c0e350e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
@@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.sync.SyncPresence
import org.matrix.android.sdk.internal.session.sync.SyncTask
-import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@@ -58,7 +57,6 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
) : SessionWorkerParams
@Inject lateinit var syncTask: SyncTask
- @Inject lateinit var taskExecutor: TaskExecutor
@Inject lateinit var workManagerProvider: WorkManagerProvider
override fun injectWith(injector: SessionComponent) {
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
index 96655b849d..088e160950 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
class VersionsKtTest {
@@ -53,5 +54,20 @@ class VersionsKtTest {
Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
+ Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true
+ }
+
+ @Test
+ fun doesServerSupportThreads() {
+ Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false
+ Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false
+ Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false
+ Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
+ Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
+ Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
+ Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+ Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+ Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false
+ Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true
}
}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
index c8be0f5487..31fd86fe65 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -16,7 +16,8 @@
package org.matrix.android.sdk.internal.session.pushers
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertFailsWith
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
@@ -39,6 +40,7 @@ private val A_JSON_PUSHER = JsonPusher(
data = JsonPusherData(brand = "Element")
)
+@ExperimentalCoroutinesApi
class DefaultAddPusherTaskTest {
private val pushersAPI = FakePushersAPI()
@@ -55,7 +57,7 @@ class DefaultAddPusherTaskTest {
fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
monarchy.givenWhereReturns(result = null)
- runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+ runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
pushersAPI.verifySetPusher(A_JSON_PUSHER)
monarchy.verifyInsertOrUpdate {
@@ -70,7 +72,7 @@ class DefaultAddPusherTaskTest {
val realmResult = PusherEntity(appDisplayName = null)
monarchy.givenWhereReturns(result = realmResult)
- runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+ runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
pushersAPI.verifySetPusher(A_JSON_PUSHER)
@@ -85,7 +87,7 @@ class DefaultAddPusherTaskTest {
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith {
- runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+ runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
}
realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER
@@ -97,7 +99,7 @@ class DefaultAddPusherTaskTest {
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith {
- runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+ runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
}
}
}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
new file mode 100644
index 0000000000..7203f89629
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.space
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
+import org.matrix.android.sdk.test.fakes.FakeSpaceApi
+import org.matrix.android.sdk.test.fixtures.SpacesResponseFixture.aSpacesResponse
+import retrofit2.HttpException
+import retrofit2.Response
+
+@ExperimentalCoroutinesApi
+internal class DefaultResolveSpaceInfoTaskTest {
+
+ private val spaceApi = FakeSpaceApi()
+ private val globalErrorReceiver = FakeGlobalErrorReceiver()
+ private val resolveSpaceInfoTask = DefaultResolveSpaceInfoTask(spaceApi.instance, globalErrorReceiver)
+
+ @Test
+ fun `given stable endpoint works, when execute, then return stable api data`() = runTest {
+ spaceApi.givenStableEndpointReturns(response)
+
+ val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+ result shouldBeEqualTo response
+ }
+
+ @Test
+ fun `given stable endpoint fails, when execute, then fallback to unstable endpoint`() = runTest {
+ spaceApi.givenStableEndpointThrows(httpException)
+ spaceApi.givenUnstableEndpointReturns(response)
+
+ val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+ result shouldBeEqualTo response
+ }
+
+ companion object {
+ private val response = aSpacesResponse()
+ private val httpException = HttpException(Response.error(500, "".toResponseBody()))
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
index 0abca8bee3..149b964fd2 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.matrix.android.sdk.MatrixTest
@@ -51,7 +51,7 @@ class CoroutineSequencersTest : MatrixTest {
.also { results.add(it) }
}
)
- runBlocking {
+ runTest {
jobs.joinAll()
}
assertEquals(3, results.size)
@@ -81,7 +81,7 @@ class CoroutineSequencersTest : MatrixTest {
.also { results.add(it) }
}
)
- runBlocking {
+ runTest {
jobs.joinAll()
}
assertEquals(3, results.size)
@@ -109,7 +109,7 @@ class CoroutineSequencersTest : MatrixTest {
)
// We are canceling the second job
jobs[1].cancel()
- runBlocking {
+ runTest {
jobs.joinAll()
}
assertEquals(2, results.size)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
new file mode 100644
index 0000000000..d4fc986791
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.space.SpaceApi
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+import org.matrix.android.sdk.test.fixtures.ResolveSpaceInfoTaskParamsFixture
+
+internal class FakeSpaceApi {
+
+ val instance: SpaceApi = mockk()
+ val params = ResolveSpaceInfoTaskParamsFixture.aResolveSpaceInfoTaskParams()
+
+ fun givenStableEndpointReturns(response: SpacesResponse) {
+ coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+ }
+
+ fun givenStableEndpointThrows(throwable: Throwable) {
+ coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws throwable
+ }
+
+ fun givenUnstableEndpointReturns(response: SpacesResponse) {
+ coEvery { instance.getSpaceHierarchyUnstable(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
new file mode 100644
index 0000000000..28f8c3637d
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.ResolveSpaceInfoTask
+
+internal object ResolveSpaceInfoTaskParamsFixture {
+ fun aResolveSpaceInfoTaskParams(
+ spaceId: String = "",
+ limit: Int? = null,
+ maxDepth: Int? = null,
+ from: String? = null,
+ suggestedOnly: Boolean? = null,
+ ) = ResolveSpaceInfoTask.Params(
+ spaceId,
+ limit,
+ maxDepth,
+ from,
+ suggestedOnly,
+ )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
new file mode 100644
index 0000000000..0a08331114
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.SpaceChildSummaryResponse
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+
+internal object SpacesResponseFixture {
+ fun aSpacesResponse(
+ nextBatch: String? = null,
+ rooms: List? = null,
+ ) = SpacesResponse(
+ nextBatch,
+ rooms,
+ )
+}
diff --git a/tools/ci/render_test_output.py b/tools/ci/render_test_output.py
index 48dd3987a3..f955b93cf9 100755
--- a/tools/ci/render_test_output.py
+++ b/tools/ci/render_test_output.py
@@ -9,37 +9,39 @@ import sys
import xml.etree.ElementTree as ET
suitename = sys.argv[1]
xmlfiles = sys.argv[2:]
-
-print(f"Arguments: {sys.argv}")
-
+print("::group::Arguments")
+print(f"{sys.argv}")
+print("::endgroup::")
for xmlfile in xmlfiles:
- print(f"Handling: {xmlfile}")
- tree = ET.parse(xmlfile)
+ try:
+ tree = ET.parse(xmlfile)
- root = tree.getroot()
- name = root.attrib['name']
- time = root.attrib['time']
- tests = int(root.attrib['tests'])
- skipped = int(root.attrib['skipped'])
- errors = int(root.attrib['errors'])
- failures = int(root.attrib['failures'])
- success = tests - failures - errors - skipped
- total = tests - skipped
- print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
-
- for testcase in root:
- if testcase.tag != "testcase":
- continue
- testname = testcase.attrib['classname']
- message = testcase.attrib['name']
- time = testcase.attrib['time']
- child = testcase.find("failure")
- if child is None:
- print(f"{message} in {time}s")
- else:
- print(f"::error file={testname}::{message} in {time}s")
- print(child.text)
- body = f"passed={success} failures={failures} errors={errors} skipped={skipped}"
- print(f"::set-output name={suitename}::={body}")
+ root = tree.getroot()
+ name = root.attrib['name']
+ time = root.attrib['time']
+ tests = int(root.attrib['tests'])
+ skipped = int(root.attrib['skipped'])
+ errors = int(root.attrib['errors'])
+ failures = int(root.attrib['failures'])
+ success = tests - failures - errors - skipped
+ total = tests - skipped
+ print(f"::group::{name} {success}/{total} ({skipped} skipped) in {time}")
+
+ for testcase in root:
+ if testcase.tag != "testcase":
+ continue
+ testname = testcase.attrib['classname']
+ message = testcase.attrib['name']
+ time = testcase.attrib['time']
+ child = testcase.find("failure")
+ if child is None:
+ print(f"{message} in {time}s")
+ else:
+ print(f"::error file={testname}::{message} in {time}s")
+ print(child.text)
+ body = f" passed={success} failures={failures} errors={errors} skipped={skipped}"
+ print(f"::set-output name={suitename}::={body}")
+ except FileNotFoundError:
+ print(f"::error::Unable to open test results file {xmlfile} - check if the tests completed")
print("::endgroup::")
diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json
index c1aa590003..a1b944a7eb 100644
--- a/tools/emojis/emoji_picker_datasource_formatted.json
+++ b/tools/emojis/emoji_picker_datasource_formatted.json
@@ -2056,7 +2056,9 @@
"disappear",
"dissolve",
"liquid",
- "melt"
+ "melt",
+ "hot",
+ "heat"
]
},
"winking-face": {
@@ -2351,7 +2353,10 @@
"disbelief",
"embarrass",
"scared",
- "surprise"
+ "surprise",
+ "silence",
+ "secret",
+ "shock"
]
},
"face-with-peeking-eye": {
@@ -2360,7 +2365,10 @@
"j": [
"captivated",
"peep",
- "stare"
+ "stare",
+ "scared",
+ "frightening",
+ "embarrassing"
]
},
"shushing-face": {
@@ -2392,7 +2400,8 @@
"salute",
"sunny",
"troops",
- "yes"
+ "yes",
+ "respect"
]
},
"zippermouth-face": {
@@ -2467,7 +2476,10 @@
"disappear",
"hide",
"introvert",
- "invisible"
+ "invisible",
+ "lonely",
+ "isolation",
+ "depression"
]
},
"face-in-clouds": {
@@ -2863,7 +2875,11 @@
"disappointed",
"meh",
"skeptical",
- "unsure"
+ "unsure",
+ "skeptic",
+ "confuse",
+ "frustrated",
+ "indifferent"
]
},
"worried-face": {
@@ -2969,7 +2985,9 @@
"cry",
"proud",
"resist",
- "sad"
+ "sad",
+ "touched",
+ "gratitude"
]
},
"frowning-face-with-open-mouth": {
@@ -4065,7 +4083,9 @@
"j": [
"hand",
"right",
- "rightward"
+ "rightward",
+ "palm",
+ "offer"
]
},
"leftwards-hand": {
@@ -4074,7 +4094,9 @@
"j": [
"hand",
"left",
- "leftward"
+ "leftward",
+ "palm",
+ "offer"
]
},
"palm-down-hand": {
@@ -4083,7 +4105,8 @@
"j": [
"dismiss",
"drop",
- "shoo"
+ "shoo",
+ "palm"
]
},
"palm-up-hand": {
@@ -4093,7 +4116,9 @@
"beckon",
"catch",
"come",
- "offer"
+ "offer",
+ "lift",
+ "demand"
]
},
"ok-hand": {
@@ -4290,7 +4315,8 @@
"b": "1FAF5",
"j": [
"point",
- "you"
+ "you",
+ "recruit"
]
},
"thumbs-up": {
@@ -4404,7 +4430,9 @@
"a": "⊛ Heart Hands",
"b": "1FAF6",
"j": [
- "love"
+ "love",
+ "appreciation",
+ "support"
]
},
"open-hands": {
@@ -4662,7 +4690,11 @@
"flirting",
"nervous",
"uncomfortable",
- "worried"
+ "worried",
+ "flirt",
+ "sexy",
+ "pain",
+ "worry"
]
},
"baby": {
@@ -6058,7 +6090,8 @@
"monarch",
"noble",
"regal",
- "royalty"
+ "royalty",
+ "power"
]
},
"prince": {
@@ -6231,7 +6264,8 @@
"belly",
"bloated",
"full",
- "pregnant"
+ "pregnant",
+ "baby"
]
},
"pregnant-person": {
@@ -6241,7 +6275,8 @@
"belly",
"bloated",
"full",
- "pregnant"
+ "pregnant",
+ "baby"
]
},
"breastfeeding": {
@@ -6635,7 +6670,8 @@
"j": [
"fairy tale",
"fantasy",
- "monster"
+ "monster",
+ "mystical"
]
},
"person-getting-massage": {
@@ -9374,7 +9410,8 @@
"b": "1FAB8",
"j": [
"ocean",
- "reef"
+ "reef",
+ "sea"
]
},
"snail": {
@@ -9587,7 +9624,9 @@
"Hinduism",
"India",
"purity",
- "Vietnam"
+ "Vietnam",
+ "calm",
+ "meditation"
]
},
"rosette": {
@@ -9832,14 +9871,16 @@
"a": "⊛ Empty Nest",
"b": "1FAB9",
"j": [
- "nesting"
+ "nesting",
+ "bird"
]
},
"nest-with-eggs": {
"a": "⊛ Nest with Eggs",
"b": "1FABA",
"j": [
- "nesting"
+ "nesting",
+ "bird"
]
},
"grapes": {
@@ -11187,7 +11228,9 @@
"drink",
"empty",
"glass",
- "spill"
+ "spill",
+ "cup",
+ "water"
]
},
"cup-with-straw": {
@@ -12003,7 +12046,9 @@
"b": "1F6DD",
"j": [
"amusement park",
- "play"
+ "play",
+ "fun",
+ "park"
]
},
"ferris-wheel": {
@@ -12533,7 +12578,9 @@
"j": [
"circle",
"tire",
- "turn"
+ "turn",
+ "car",
+ "transport"
]
},
"police-car-light": {
@@ -14666,7 +14713,8 @@
"hand",
"Mary",
"Miriam",
- "protection"
+ "protection",
+ "religion"
]
},
"video-game": {
@@ -15864,7 +15912,9 @@
"b": "1FAAB",
"j": [
"electronic",
- "low energy"
+ "low energy",
+ "drained",
+ "dead"
]
},
"electric-plug": {
@@ -17508,7 +17558,9 @@
"disability",
"hurt",
"mobility aid",
- "stick"
+ "stick",
+ "accessibility",
+ "assist"
]
},
"stethoscope": {
@@ -17528,7 +17580,9 @@
"bones",
"doctor",
"medical",
- "skeleton"
+ "skeleton",
+ "x-ray",
+ "medicine"
]
},
"door": {
@@ -17733,7 +17787,10 @@
"burp",
"clean",
"soap",
- "underwater"
+ "underwater",
+ "fun",
+ "carbonation",
+ "sparkling"
]
},
"toothbrush": {
@@ -17856,7 +17913,8 @@
"credentials",
"ID",
"license",
- "security"
+ "security",
+ "document"
]
},
"atm-sign": {
diff --git a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl
index 64e6a0f83f..62b1f40df5 100644
--- a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl
+++ b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl
@@ -7,7 +7,6 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
-import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
<#if createViewEvents>
@@ -42,6 +41,6 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi
override fun handle(action: ${actionClass}) {
when (action) {
- }.exhaustive
+ }
}
}
diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml
index 0121ee9ae7..40fc68bbae 100755
--- a/vector-config/src/main/res/values/config-settings.xml
+++ b/vector-config/src/main/res/values/config-settings.xml
@@ -36,8 +36,9 @@
+ false
-
+
diff --git a/vector/build.gradle b/vector/build.gradle
index 676b84839f..9f8471bc18 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -18,7 +18,7 @@ ext.versionMinor = 4
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
-ext.versionPatch = 4
+ext.versionPatch = 8
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -131,6 +131,9 @@ android {
// Required for sonar analysis
versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar"
+ // Generate a random app task affinity
+ manifestPlaceholders = [appTaskAffinitySuffix:"H_${gitRevision()}"]
+
buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\""
buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\""
@@ -148,6 +151,7 @@ android {
buildConfigField "Boolean", "enableLocationSharing", "true"
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
+ buildConfigField "Boolean", "PRESENCE_SYNC_ENABLED", "true"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -226,6 +230,7 @@ android {
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
// Set to true if you want to enable strict mode in debug
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
+ buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "true"
signingConfig signingConfigs.debug
}
@@ -235,6 +240,7 @@ android {
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
+ buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "false"
postprocessing {
removeUnusedCode true
@@ -352,6 +358,7 @@ dependencies {
// Lifecycle
implementation libs.androidx.lifecycleLivedata
implementation libs.androidx.lifecycleProcess
+ implementation libs.androidx.lifecycleRuntimeKtx
implementation libs.androidx.datastore
implementation libs.androidx.datastorepreferences
@@ -364,7 +371,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45'
// FlowBinding
implementation libs.github.flowBinding
@@ -404,7 +411,6 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
implementation 'com.github.hyuwah:DraggableView:1.0.0'
- implementation 'com.github.Armen101:AudioRecordView:1.0.5'
// Custom Tab
implementation 'androidx.browser:browser:1.4.0'
diff --git a/vector/lint.xml b/vector/lint.xml
index 3515097379..e219ac1eed 100644
--- a/vector/lint.xml
+++ b/vector/lint.xml
@@ -6,6 +6,7 @@
+
@@ -14,24 +15,14 @@
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
index 417d28d625..5a03d5890a 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
@@ -27,6 +27,7 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
import im.vector.app.getString
import im.vector.app.ui.robot.ElementRobot
+import im.vector.app.ui.robot.settings.labs.LabFeature
import im.vector.app.ui.robot.withDeveloperMode
import org.junit.Rule
import org.junit.Test
@@ -97,6 +98,8 @@ class UiAllScreensSanityTest {
}
}
+ testThreadScreens()
+
elementRobot.space {
createSpace {
crawl()
@@ -148,4 +151,25 @@ class UiAllScreensSanityTest {
// TODO Deactivate account instead of logout?
elementRobot.signout(expectSignOutWarning = false)
}
+
+ /**
+ * Testing multiple threads screens
+ */
+ private fun testThreadScreens() {
+ elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
+ elementRobot.newRoom {
+ createNewRoom {
+ crawl()
+ createRoom {
+ val message = "Hello This message will be a thread!"
+ postMessage(message)
+ replyToThread(message)
+ viewInRoom(message)
+ openThreadSummaries()
+ selectThreadSummariesFilter()
+ }
+ }
+ }
+ elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
+ }
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
index f0ce23b7db..3c5de8b221 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
@@ -17,9 +17,15 @@
package im.vector.app.ui.robot
import android.view.View
+import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
@@ -35,6 +41,7 @@ import im.vector.app.features.home.HomeActivity
import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.initialSyncIdlingResource
import im.vector.app.ui.robot.settings.SettingsRobot
+import im.vector.app.ui.robot.settings.labs.LabFeature
import im.vector.app.ui.robot.space.SpaceRobot
import im.vector.app.withIdlingResource
import timber.log.Timber
@@ -70,11 +77,11 @@ class ElementRobot {
}
}
- fun settings(block: SettingsRobot.() -> Unit) {
+ fun settings(shouldGoBack: Boolean = true, block: SettingsRobot.() -> Unit) {
openDrawer()
clickOn(R.id.homeDrawerHeaderSettingsView)
block(SettingsRobot())
- pressBack()
+ if (shouldGoBack) pressBack()
waitUntilViewVisible(withId(R.id.bottomNavigationView))
}
@@ -103,6 +110,22 @@ class ElementRobot {
waitUntilViewVisible(withId(R.id.bottomNavigationView))
}
+ fun toggleLabFeature(labFeature: LabFeature) {
+ when (labFeature) {
+ LabFeature.THREAD_MESSAGES -> {
+ settings(shouldGoBack = false) {
+ labs(shouldGoBack = false) {
+ onView(withText(R.string.labs_enable_thread_messages))
+ .check(ViewAssertions.matches(isDisplayed()))
+ .perform(ViewActions.closeSoftKeyboard(), click())
+ }
+ }
+ }
+ else -> {
+ }
+ }
+ }
+
fun signout(expectSignOutWarning: Boolean) {
clickOn(R.id.groupToolbarAvatarImageView)
clickOn(R.id.homeDrawerHeaderSignoutView)
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
index 5973dc3473..5c9ecfdef5 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
@@ -70,4 +70,13 @@ class MessageMenuRobot(
clickOn(R.string.edit)
autoClosed = true
}
+
+ fun replyInThread() {
+ clickOn(R.string.reply_in_thread)
+ autoClosed = true
+ }
+ fun viewInRoom() {
+ clickOn(R.string.view_in_room)
+ autoClosed = true
+ }
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
index b3bb5172e8..d051488ad7 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt
@@ -21,6 +21,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertEnabled
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
@@ -55,6 +56,8 @@ class OnboardingRobot {
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl)
+ waitUntilViewVisible(withText(R.string.ftue_account_created_congratulations_title))
+ clickOn(R.string.ftue_account_created_take_me_home)
}
fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
index 6cf6ad3551..91409582d9 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
@@ -62,6 +62,23 @@ class RoomDetailRobot {
pressBack()
}
+ fun replyToThread(message: String) {
+ openMessageMenu(message) {
+ replyInThread()
+ }
+ val threadMessage = "Hello universe - long message to avoid espresso tapping edited!"
+ writeTo(R.id.composerEditText, threadMessage)
+ waitUntilViewVisible(withId(R.id.sendButton))
+ clickOn(R.id.sendButton)
+ }
+
+ fun viewInRoom(message: String) {
+ openMessageMenu(message) {
+ viewInRoom()
+ }
+ waitUntilViewVisible(withId(R.id.composerEditText))
+ }
+
fun crawlMessage(message: String) {
// Test quick reaction
val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
@@ -110,7 +127,7 @@ class RoomDetailRobot {
onView(withId(R.id.timelineRecyclerView))
.perform(
RecyclerViewActions.actionOnItem(
- ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
+ ViewMatchers.hasDescendant(withText(message)),
ViewActions.longClick()
)
)
@@ -130,4 +147,16 @@ class RoomDetailRobot {
block(RoomSettingsRobot())
pressBack()
}
+
+ fun openThreadSummaries() {
+ clickMenu(R.id.menu_timeline_thread_list)
+ waitUntilViewVisible(withId(R.id.threadListRecyclerView))
+ }
+
+ fun selectThreadSummariesFilter() {
+ clickMenu(R.id.menu_thread_list_filter)
+ sleep(1000)
+ clickOn(R.id.threadListModalMyThreads)
+ pressBack()
+ }
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
index 561f14c6f2..97aee7ac4a 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
@@ -16,6 +16,7 @@
package im.vector.app.ui.robot.settings
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import im.vector.app.R
import im.vector.app.clickOnAndGoBack
@@ -51,8 +52,13 @@ class SettingsRobot {
clickOnAndGoBack(R.string.settings_security_and_privacy) { block(SettingsSecurityRobot()) }
}
- fun labs(block: () -> Unit = {}) {
- clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
+ fun labs(shouldGoBack: Boolean = true, block: () -> Unit = {}) {
+ if (shouldGoBack) {
+ clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
+ } else {
+ clickOn(R.string.room_settings_labs_pref_title)
+ block()
+ }
}
fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt
new file mode 100644
index 0000000000..656201d812
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 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.ui.robot.settings.labs
+
+enum class LabFeature {
+ SWIPE_TO_REPLY,
+ TAB_UNREAD_NOTIFICATIONS,
+ LATEX_MATHEMATICS,
+ THREAD_MESSAGES,
+ AUTO_REPORT_ERRORS,
+ RENDER_USER_LOCATION
+}
diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml
index 0b2b5cf90f..87aade0c8b 100644
--- a/vector/src/debug/AndroidManifest.xml
+++ b/vector/src/debug/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt
index 03e416813a..e007e61c1c 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt
@@ -22,7 +22,6 @@ 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.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.store.AnalyticsStore
@@ -53,7 +52,7 @@ class DebugAnalyticsViewModel @AssistedInject constructor(
override fun handle(action: DebugAnalyticsViewActions) {
when (action) {
DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed -> handleResetAnalyticsOptInDisplayed()
- }.exhaustive
+ }
}
private fun handleResetAnalyticsOptInDisplayed() {
diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt
index 0c4a3ef637..3a68a0b956 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt
@@ -23,8 +23,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.features.DefaultVectorFeatures
+import im.vector.app.features.DefaultVectorOverrides
import im.vector.app.features.VectorFeatures
+import im.vector.app.features.VectorOverrides
import im.vector.app.features.debug.features.DebugVectorFeatures
+import im.vector.app.features.debug.features.DebugVectorOverrides
@InstallIn(SingletonComponent::class)
@Module
@@ -33,6 +36,9 @@ interface FeaturesModule {
@Binds
fun bindFeatures(debugFeatures: DebugVectorFeatures): VectorFeatures
+ @Binds
+ fun bindOverrides(debugOverrides: DebugVectorOverrides): VectorOverrides
+
companion object {
@Provides
@@ -44,5 +50,15 @@ interface FeaturesModule {
fun providesDebugVectorFeatures(context: Context, defaultVectorFeatures: DefaultVectorFeatures): DebugVectorFeatures {
return DebugVectorFeatures(context, defaultVectorFeatures)
}
+
+ @Provides
+ fun providesDefaultVectorOverrides(): DefaultVectorOverrides {
+ return DefaultVectorOverrides()
+ }
+
+ @Provides
+ fun providesDebugVectorOverrides(context: Context): DebugVectorOverrides {
+ return DebugVectorOverrides(context)
+ }
}
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt
new file mode 100644
index 0000000000..5e16182f3c
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2022 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.features
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.preferencesDataStore
+import im.vector.app.features.HomeserverCapabilitiesOverride
+import im.vector.app.features.VectorOverrides
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.extensions.orFalse
+
+private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_overrides")
+private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display")
+private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback")
+private val forceCanChangeDisplayName = booleanPreferencesKey("force_can_change_display_name")
+private val forceCanChangeAvatar = booleanPreferencesKey("force_can_change_avatar")
+
+class DebugVectorOverrides(private val context: Context) : VectorOverrides {
+
+ override val forceDialPad = context.dataStore.data.map { preferences ->
+ preferences[keyForceDialPadDisplay].orFalse()
+ }
+
+ override val forceLoginFallback = context.dataStore.data.map { preferences ->
+ preferences[keyForceLoginFallback].orFalse()
+ }
+
+ override val forceHomeserverCapabilities = context.dataStore.data.map { preferences ->
+ HomeserverCapabilitiesOverride(
+ canChangeDisplayName = preferences[forceCanChangeDisplayName],
+ canChangeAvatar = preferences[forceCanChangeAvatar]
+ )
+ }
+
+ suspend fun setForceDialPadDisplay(force: Boolean) {
+ context.dataStore.edit { settings ->
+ settings[keyForceDialPadDisplay] = force
+ }
+ }
+
+ suspend fun setForceLoginFallback(force: Boolean) {
+ context.dataStore.edit { settings ->
+ settings[keyForceLoginFallback] = force
+ }
+ }
+
+ suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) {
+ val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null))
+ context.dataStore.edit { settings ->
+ when (capabilitiesOverride.canChangeDisplayName) {
+ null -> settings.remove(forceCanChangeDisplayName)
+ else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName
+ }
+ when (capabilitiesOverride.canChangeAvatar) {
+ null -> settings.remove(forceCanChangeAvatar)
+ else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar
+ }
+ }
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
index b54d776901..38253fe7c2 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
@@ -50,6 +50,12 @@ class DebugPrivateSettingsFragment : VectorBaseFragment
+ viewModel.handle(DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride(option))
+ }
+ views.forceChangeAvatarCapability.bind(it.homeserverCapabilityOverrides.avatar) { option ->
+ viewModel.handle(DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride(option))
+ }
views.forceLoginFallback.isChecked = it.forceLoginFallback
}
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
index 1c76cf6fb2..5dea3dce64 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
@@ -18,7 +18,9 @@ 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()
- data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
+sealed interface DebugPrivateSettingsViewActions : VectorViewModelAction {
+ data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions
+ data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions
+ data class SetDisplayNameCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
+ data class SetAvatarCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
index 038b1e6cc7..e469dbacda 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
@@ -24,12 +24,14 @@ 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 im.vector.app.features.debug.features.DebugVectorOverrides
+import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride
+import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride
import kotlinx.coroutines.launch
class DebugPrivateSettingsViewModel @AssistedInject constructor(
@Assisted initialState: DebugPrivateSettingsViewState,
- private val vectorDataStore: VectorDataStore
+ private val debugVectorOverrides: DebugVectorOverrides
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -40,35 +42,60 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
init {
- observeVectorDataStore()
+ observeVectorOverrides()
}
- private fun observeVectorDataStore() {
- vectorDataStore.forceDialPadDisplayFlow.setOnEach {
- copy(dialPadVisible = it)
+ private fun observeVectorOverrides() {
+ debugVectorOverrides.forceDialPad.setOnEach {
+ copy(
+ dialPadVisible = it
+ )
}
-
- vectorDataStore.forceLoginFallbackFlow.setOnEach {
+ debugVectorOverrides.forceLoginFallback.setOnEach {
copy(forceLoginFallback = it)
}
+ debugVectorOverrides.forceHomeserverCapabilities.setOnEach {
+ val activeDisplayNameOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeDisplayName)
+ val activeAvatarOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeAvatar)
+ copy(homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy(
+ displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption),
+ avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption),
+ ))
+ }
}
override fun handle(action: DebugPrivateSettingsViewActions) {
when (action) {
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action)
+ is SetDisplayNameCapabilityOverride -> handSetDisplayNameCapabilityOverride(action)
+ is SetAvatarCapabilityOverride -> handSetAvatarCapabilityOverride(action)
}
}
private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) {
viewModelScope.launch {
- vectorDataStore.setForceDialPadDisplay(action.force)
+ debugVectorOverrides.setForceDialPadDisplay(action.force)
}
}
private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) {
viewModelScope.launch {
- vectorDataStore.setForceLoginFallbackFlow(action.force)
+ debugVectorOverrides.setForceLoginFallback(action.force)
+ }
+ }
+
+ private fun handSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) {
+ viewModelScope.launch {
+ val forceDisplayName = action.option.toBoolean()
+ debugVectorOverrides.setHomeserverCapabilities { copy(canChangeDisplayName = forceDisplayName) }
+ }
+ }
+
+ private fun handSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) {
+ viewModelScope.launch {
+ val forceAvatar = action.option.toBoolean()
+ debugVectorOverrides.setHomeserverCapabilities { copy(canChangeAvatar = forceAvatar) }
}
}
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
index 7fca29af8c..749b11a744 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
@@ -17,8 +17,23 @@
package im.vector.app.features.debug.settings
import com.airbnb.mvrx.MavericksState
+import im.vector.app.features.debug.settings.OverrideDropdownView.OverrideDropdown
data class DebugPrivateSettingsViewState(
val dialPadVisible: Boolean = false,
val forceLoginFallback: Boolean = false,
+ val homeserverCapabilityOverrides: HomeserverCapabilityOverrides = HomeserverCapabilityOverrides()
) : MavericksState
+
+data class HomeserverCapabilityOverrides(
+ val displayName: OverrideDropdown