diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index a5bb7c2a6c..d8c1bb6c49 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -14,50 +14,6 @@ env:
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=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@v3
- - 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@v3
- - 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
@@ -87,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.3.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
@@ -274,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.3.0
+ with:
+ uploadLogs: true
+ httpPort: 8080
+ disableRateLimiting: true
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
@@ -366,9 +323,6 @@ jobs:
needs:
- integration-tests
- ui-tests
-# - unit-tests
- - build-android-test-matrix-sdk
- - build-android-test-app
- sonarqube
if: always() && github.event_name != 'workflow_dispatch'
# No concurrency required, runs every time on a schedule.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d739afcd30..587bf14488 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@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
+ run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
+
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
@@ -41,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/.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..c411593627 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,49 @@
+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/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/4498.misc b/changelog.d/4498.misc
deleted file mode 100644
index 78493b5d77..0000000000
--- a/changelog.d/4498.misc
+++ /dev/null
@@ -1 +0,0 @@
-Override task affinity to prevent unknown activities running in our app tasks.
\ No newline at end of file
diff --git a/changelog.d/4533.misc b/changelog.d/4533.misc
new file mode 100644
index 0000000000..1137a1c43c
--- /dev/null
+++ b/changelog.d/4533.misc
@@ -0,0 +1 @@
+Improve headers UI in Rooms/Messages lists
diff --git a/changelog.d/4642.bugfix b/changelog.d/4642.bugfix
deleted file mode 100644
index 2a5ea97196..0000000000
--- a/changelog.d/4642.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Update the top bar in a room: remove topic and typing information
\ No newline at end of file
diff --git a/changelog.d/4860.bugfix b/changelog.d/4860.bugfix
new file mode 100644
index 0000000000..32049face4
--- /dev/null
+++ b/changelog.d/4860.bugfix
@@ -0,0 +1 @@
+Add colors for shield vector drawable
\ No newline at end of file
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/5346.misc b/changelog.d/5346.misc
new file mode 100644
index 0000000000..f979c180ef
--- /dev/null
+++ b/changelog.d/5346.misc
@@ -0,0 +1 @@
+Selected space highlight changed in left panel
\ 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/5361.misc b/changelog.d/5361.misc
deleted file mode 100644
index d49554c7e7..0000000000
--- a/changelog.d/5361.misc
+++ /dev/null
@@ -1 +0,0 @@
-Creates dedicated VectorOverrides for forcing behaviour for local testing/development
\ No newline at end of file
diff --git a/changelog.d/5375.wip b/changelog.d/5375.wip
new file mode 100644
index 0000000000..352b2385a9
--- /dev/null
+++ b/changelog.d/5375.wip
@@ -0,0 +1 @@
+Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities
\ 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/5384.misc b/changelog.d/5384.misc
new file mode 100644
index 0000000000..dca87422bb
--- /dev/null
+++ b/changelog.d/5384.misc
@@ -0,0 +1 @@
+Add top margin before our first message
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/5395.feature b/changelog.d/5395.feature
new file mode 100644
index 0000000000..eb16c6cd81
--- /dev/null
+++ b/changelog.d/5395.feature
@@ -0,0 +1 @@
+Add a custom view to display a picker for share location options
diff --git a/changelog.d/5402.bugfix b/changelog.d/5402.bugfix
deleted file mode 100644
index fde9e7e74f..0000000000
--- a/changelog.d/5402.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-[Create room] Setting an avatar when creating a room had no effect
\ No newline at end of file
diff --git a/changelog.d/5418.feature b/changelog.d/5418.feature
deleted file mode 100644
index 5e1efc8718..0000000000
--- a/changelog.d/5418.feature
+++ /dev/null
@@ -1 +0,0 @@
-Improves settings error dialog messaging when changing avatar or display name fails
\ No newline at end of file
diff --git a/changelog.d/5443.misc b/changelog.d/5443.misc
new file mode 100644
index 0000000000..f9fd715403
--- /dev/null
+++ b/changelog.d/5443.misc
@@ -0,0 +1 @@
+Adds stable room hierarchy endpoint with a fallback to the unstable one
diff --git a/changelog.d/5448.bugfix b/changelog.d/5448.bugfix
new file mode 100644
index 0000000000..c4e8fb4a49
--- /dev/null
+++ b/changelog.d/5448.bugfix
@@ -0,0 +1 @@
+Fix missing messages when loading messages forwards
diff --git a/changelog.d/5456.misc b/changelog.d/5456.misc
deleted file mode 100644
index 94746ca788..0000000000
--- a/changelog.d/5456.misc
+++ /dev/null
@@ -1 +0,0 @@
-Iterate on badge / unread indicator color
\ No newline at end of file
diff --git a/changelog.d/5501.misc b/changelog.d/5501.misc
new file mode 100644
index 0000000000..6c46a105b7
--- /dev/null
+++ b/changelog.d/5501.misc
@@ -0,0 +1 @@
+Use ColorPrimary for attachmentGalleryButton tint
\ No newline at end of file
diff --git a/changelog.d/5514.bugfix b/changelog.d/5514.bugfix
new file mode 100644
index 0000000000..0dfbca6e9a
--- /dev/null
+++ b/changelog.d/5514.bugfix
@@ -0,0 +1 @@
+Read receipt in wrong order
\ No newline at end of file
diff --git a/dependencies.gradle b/dependencies.gradle
index 87b8e3c12f..1f2a08b6a6 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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/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/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/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/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 6610c0f45d..75b03a7d2e 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
@@ -83,16 +78,6 @@
#BF000000
#BF000000
-
- #FFFFFFFF
- #FF22262E
- #FF090A0C
-
-
- #FFE9EDF1
- #FF22262E
- #FF090A0C
-
#EBEFF5
#27303A
@@ -107,9 +92,7 @@
#AAAAAAAA
#55555555
-
#EEEEEE
- #61708B
#FFF3F8FD
@@ -139,4 +122,14 @@
@color/palette_gray_100
@color/palette_gray_450
+
+
+ @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 db42cfa12c..6737f4faf1 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
@@ -70,4 +61,7 @@
- 0.15
- 0.05
-
\ No newline at end of file
+
+
+ 10dp
+
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..ec2f1d0814 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
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/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/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 607f008453..06670ccd68 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
@@ -145,6 +143,8 @@
- @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 efc18b9f32..c184464320 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
@@ -146,6 +144,8 @@
- @style/Widget.Vector.ActionButton
+
+ - @color/vctr_live_location_light
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 3e301eebb9..2b2c38e22a 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.6\""
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'
@@ -175,7 +174,7 @@ dependencies {
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation libs.mockk.mockk
testImplementation libs.tests.kluent
- implementation libs.jetbrains.coroutinesAndroid
+ testImplementation libs.jetbrains.coroutinesTest
// Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// Transitively required for mocking realm as monarchy doesn't expose Rx
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/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/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/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index bca432320d..f506b147df 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
+import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
@@ -216,6 +217,11 @@ interface RoomService {
pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
+ /**
+ * Retrieve a flow on the number of rooms.
+ */
+ fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow
+
/**
* 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/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/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/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/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 4a02c55db0..0d78489fbd 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
@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
+import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService
@@ -109,6 +110,10 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
}
+ override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow {
+ return roomSummaryDataSource.getCountFlow(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/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e0d501c515..cd06d47f05 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
@@ -156,7 +156,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
* 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/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index c9fc3c9575..ea4f102fa5 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
@@ -25,7 +25,13 @@ import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
+import io.realm.kotlin.toFlow
import io.realm.kotlin.where
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+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 +48,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 +62,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,17 +228,29 @@ 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 getCountFlow(queryParams: RoomSummaryQueryParams): Flow =
+ realmSessionProvider
+ .withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() }
+ .toFlow()
+ // need to create the flow on a context dispatcher with a thread with attached Looper
+ .flowOn(coroutineDispatchers.main)
+ .map { it.size }
+ .flowOn(coroutineDispatchers.io)
+ .distinctUntilChanged()
+
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
var notificationCount: RoomAggregateNotificationCount? = null
monarchy.doWithRealm { realm ->
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 449fe60dca..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) }
}
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..1262c09d97 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
@@ -66,7 +66,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 +76,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..77f210aa9a 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
@@ -144,14 +144,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
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)
}
@@ -508,13 +508,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 {
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..bacac58d84 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,9 @@ internal class TimelineEventDecryptor @Inject constructor(
}
executor?.execute {
Realm.getInstance(realmConfiguration).use { realm ->
- processDecryptRequest(request, realm)
+ runBlocking {
+ processDecryptRequest(request, realm)
+ }
}
}
}
@@ -115,7 +118,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/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/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/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 99e6521eb7..a5ad19bbf8 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,6 +19,7 @@ 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
@@ -379,7 +380,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) {
@@ -455,7 +458,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return chunkEntity
}
- 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/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..f80c0f06d0
--- /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.runBlockingTest
+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`() = runBlockingTest {
+ 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`() = runBlockingTest {
+ 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/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/vector/build.gradle b/vector/build.gradle
index c4c9436e13..2d9c097da8 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 = 6
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -355,6 +355,7 @@ dependencies {
// Lifecycle
implementation libs.androidx.lifecycleLivedata
implementation libs.androidx.lifecycleProcess
+ implementation libs.androidx.lifecycleRuntimeKtx
implementation libs.androidx.datastore
implementation libs.androidx.datastorepreferences
@@ -367,7 +368,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
diff --git a/vector/lint.xml b/vector/lint.xml
index 22da6adfa9..e219ac1eed 100644
--- a/vector/lint.xml
+++ b/vector/lint.xml
@@ -15,24 +15,14 @@
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
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
index 4394f5436e..5e16182f3c 100644
--- 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
@@ -22,13 +22,17 @@ 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 {
@@ -40,6 +44,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
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
@@ -51,4 +62,18 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
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 8d040d4773..62871023bc 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
@@ -22,9 +22,12 @@ 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.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(
@@ -40,10 +43,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
init {
- observeVectorDataStore()
+ observeVectorOverrides()
}
- private fun observeVectorDataStore() {
+ private fun observeVectorOverrides() {
debugVectorOverrides.forceDialPad.setOnEach {
copy(
dialPadVisible = it
@@ -52,13 +55,23 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
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)
+ }.exhaustive
}
private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) {
@@ -72,4 +85,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
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 = OverrideDropdown(
+ label = "Override display name capability",
+ activeOption = null,
+ options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
+ ),
+ val avatar: OverrideDropdown = OverrideDropdown(
+ label = "Override avatar capability",
+ activeOption = null,
+ options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
+ )
+)
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt
new file mode 100644
index 0000000000..48ec44f909
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.settings
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.LinearLayout
+import im.vector.app.R
+import im.vector.app.databinding.ViewBooleanDropdownBinding
+
+class OverrideDropdownView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+
+ private val binding = ViewBooleanDropdownBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+
+ init {
+ orientation = HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ }
+
+ fun bind(feature: OverrideDropdown, listener: Listener) {
+ binding.overrideLabel.text = feature.label
+
+ binding.overrideOptions.apply {
+ val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item)
+ val options = listOf("Inactive") + feature.options.map { it.label }
+ arrayAdapter.addAll(options)
+ adapter = arrayAdapter
+
+ feature.activeOption?.let {
+ setSelection(options.indexOf(it.label), false)
+ }
+
+ onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ when (position) {
+ 0 -> listener.onOverrideSelected(option = null)
+ else -> listener.onOverrideSelected(feature.options[position - 1])
+ }
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ // do nothing
+ }
+ }
+ }
+ }
+
+ fun interface Listener {
+ fun onOverrideSelected(option: T?)
+ }
+
+ data class OverrideDropdown(
+ val label: String,
+ val options: List,
+ val activeOption: T?,
+ )
+}
+
+interface OverrideOption {
+ val label: String
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt
new file mode 100644
index 0000000000..316e8fb901
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.settings
+
+sealed interface BooleanHomeserverCapabilitiesOverride : OverrideOption {
+
+ companion object {
+ fun from(value: Boolean?) = when (value) {
+ null -> null
+ true -> ForceEnabled
+ false -> ForceDisabled
+ }
+ }
+
+ object ForceEnabled : BooleanHomeserverCapabilitiesOverride {
+ override val label = "Force enabled"
+ }
+
+ object ForceDisabled : BooleanHomeserverCapabilitiesOverride {
+ override val label = "Force disabled"
+ }
+}
+
+fun BooleanHomeserverCapabilitiesOverride?.toBoolean() = when (this) {
+ null -> null
+ BooleanHomeserverCapabilitiesOverride.ForceDisabled -> false
+ BooleanHomeserverCapabilitiesOverride.ForceEnabled -> true
+}
diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
index 6760c68169..c42ad68dce 100644
--- a/vector/src/debug/res/layout/fragment_debug_private_settings.xml
+++ b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
@@ -31,6 +31,24 @@
android:layout_height="wrap_content"
android:text="Force login and registration fallback" />
+
+
+
+
diff --git a/vector/src/debug/res/layout/view_boolean_dropdown.xml b/vector/src/debug/res/layout/view_boolean_dropdown.xml
new file mode 100644
index 0000000000..5018d61047
--- /dev/null
+++ b/vector/src/debug/res/layout/view_boolean_dropdown.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
index 54fcac42d1..f9ca8cb57c 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt
@@ -23,6 +23,7 @@ import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
@@ -70,6 +71,15 @@ fun View.setAttributeTintedBackground(@DrawableRes drawableRes: Int, @AttrRes ti
background = drawable
}
+fun View.tintBackground(@ColorInt tintColor: Int) {
+ val bkg = background?.let {
+ val backgroundDrawable = DrawableCompat.wrap(background)
+ DrawableCompat.setTint(backgroundDrawable, tintColor)
+ backgroundDrawable
+ }
+ background = bkg
+}
+
fun ImageView.setAttributeTintedImageResource(@DrawableRes drawableRes: Int, @AttrRes tint: Int) {
val drawable = ContextCompat.getDrawable(context, drawableRes)!!
DrawableCompat.setTint(drawable, ThemeUtils.getColor(context, tint))
diff --git a/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt
index 4128fdbe3c..daa0d9e0bd 100644
--- a/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt
+++ b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt
@@ -22,9 +22,16 @@ import kotlinx.coroutines.flow.flowOf
interface VectorOverrides {
val forceDialPad: Flow
val forceLoginFallback: Flow
+ val forceHomeserverCapabilities: Flow?
}
+data class HomeserverCapabilitiesOverride(
+ val canChangeDisplayName: Boolean?,
+ val canChangeAvatar: Boolean?
+)
+
class DefaultVectorOverrides : VectorOverrides {
override val forceDialPad = flowOf(false)
override val forceLoginFallback = flowOf(false)
+ override val forceHomeserverCapabilities: Flow? = null
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 43fa9e0c2e..fb47fb5136 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
+import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
@@ -415,7 +416,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
partialState = partialState,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback,
- eventsGroup = timelineEventsGroup
+ eventsGroup = timelineEventsGroup,
+ reactionsSummaryEvents = ReactionsSummaryEvents(
+ onAddMoreClicked = { reactionListFactory.onAddMoreClicked(callback, event) },
+ onShowLessClicked = { reactionListFactory.onShowLessClicked(event.eventId) },
+ onShowMoreClicked = { reactionListFactory.onShowMoreClicked(event.eventId) }
+ )
)
modelCache[position] = buildCacheItem(params)
numberOfEventsToBuild++
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
index 0161f0b55d..a5d6f75387 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
@@ -26,6 +26,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
+import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -61,7 +62,8 @@ class CallItemFactory @Inject constructor(
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = callEventGrouper.isInCall(),
- formattedDuration = callEventGrouper.formattedDuration()
+ formattedDuration = callEventGrouper.formattedDuration(),
+ reactionsSummaryEvents = params.reactionsSummaryEvents
)
} else {
null
@@ -78,7 +80,8 @@ class CallItemFactory @Inject constructor(
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = callEventGrouper.isRinging(),
- formattedDuration = callEventGrouper.formattedDuration()
+ formattedDuration = callEventGrouper.formattedDuration(),
+ reactionsSummaryEvents = params.reactionsSummaryEvents
)
} else {
null
@@ -94,7 +97,8 @@ class CallItemFactory @Inject constructor(
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = false,
- formattedDuration = callEventGrouper.formattedDuration()
+ formattedDuration = callEventGrouper.formattedDuration(),
+ reactionsSummaryEvents = params.reactionsSummaryEvents
)
}
EventType.CALL_HANGUP -> {
@@ -111,7 +115,8 @@ class CallItemFactory @Inject constructor(
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = false,
- formattedDuration = callEventGrouper.formattedDuration()
+ formattedDuration = callEventGrouper.formattedDuration(),
+ reactionsSummaryEvents = params.reactionsSummaryEvents
)
}
else -> null
@@ -133,10 +138,11 @@ class CallItemFactory @Inject constructor(
highlight: Boolean,
isStillActive: Boolean,
formattedDuration: String,
- callback: TimelineEventController.Callback?
+ callback: TimelineEventController.Callback?,
+ reactionsSummaryEvents: ReactionsSummaryEvents?
): CallTileTimelineItem? {
val userOfInterest = roomSummary.toMatrixItem()
- val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
+ val attributes = messageItemAttributesFactory.create(null, informationData, callback, reactionsSummaryEvents).let {
CallTileTimelineItem.Attributes(
callId = callId,
callKind = callKind,
@@ -151,7 +157,8 @@ class CallItemFactory @Inject constructor(
readReceiptsCallback = it.readReceiptsCallback,
userOfInterest = userOfInterest,
callback = callback,
- isStillActive = isStillActive
+ isStillActive = isStillActive,
+ reactionsSummaryEvents = reactionsSummaryEvents
)
}
return CallTileTimelineItem_()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
index bc2497392c..2b04600af2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
@@ -111,7 +111,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
messageContent = event.root.content.toModel(),
informationData = informationData,
callback = params.callback,
- threadDetails = threadDetails)
+ threadDetails = threadDetails,
+ reactionsSummaryEvents = params.reactionsSummaryEvents
+ )
return MessageTextItem_()
.layout(informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
index 0ff786d504..0cb86a5c1c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
@@ -46,7 +46,7 @@ class EncryptionItemFactory @Inject constructor(
}
val algorithm = event.root.content.toModel()?.algorithm
val informationData = informationDataFactory.create(params)
- val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
+ val attributes = messageItemAttributesFactory.create(null, informationData, params.callback, params.reactionsSummaryEvents)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
val title: String
@@ -80,7 +80,8 @@ class EncryptionItemFactory @Inject constructor(
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
- readReceiptsCallback = attributes.readReceiptsCallback
+ readReceiptsCallback = attributes.readReceiptsCallback,
+ reactionsSummaryEvents = attributes.reactionsSummaryEvents
)
)
.highlighted(params.isHighlighted)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index aa1758dd6c..2890e070ef 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -155,7 +155,7 @@ class MessageItemFactory @Inject constructor(
if (event.root.isRedacted()) {
// message is redacted
- val attributes = messageItemAttributesFactory.create(null, informationData, callback, threadDetails)
+ val attributes = messageItemAttributesFactory.create(null, informationData, callback, params.reactionsSummaryEvents)
return buildRedactedItem(attributes, highlight)
}
@@ -177,7 +177,7 @@ class MessageItemFactory @Inject constructor(
}
// always hide summary when we are on thread timeline
- val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails)
+ val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails)
// val all = event.root.toContent()
// val ev = all.toModel()
@@ -413,7 +413,8 @@ class MessageItemFactory @Inject constructor(
itemClickListener = attributes.itemClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback,
- emojiTypeFace = attributes.emojiTypeFace
+ emojiTypeFace = attributes.emojiTypeFace,
+ reactionsSummaryEvents = attributes.reactionsSummaryEvents
)
)
.callback(callback)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
index e66dd4b043..ed3cc8df53 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
@@ -38,8 +38,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
.map {
ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs)
}
- .toList()
-
+ .sortedByDescending { it.timestamp }
return ReadReceiptsItem_()
.id("read_receipts_$eventId")
.eventId(eventId)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
index 46ae01a794..7c02b6f058 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
+import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams(
@@ -29,6 +30,7 @@ data class TimelineItemFactoryParams(
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
val lastSentEventIdWithoutReadReceipts: String? = null,
val callback: TimelineEventController.Callback? = null,
+ val reactionsSummaryEvents: ReactionsSummaryEvents? = null,
val eventsGroup: TimelineEventsGroup? = null
) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
index bdc6906593..16cf73cbb0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
@@ -71,10 +71,10 @@ class VerificationItemFactory @Inject constructor(
// If it's not a request ignore this event
// if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback)
- val referenceInformationData = messageInformationDataFactory.create(TimelineItemFactoryParams(refEvent))
+ val referenceInformationData = messageInformationDataFactory.create(TimelineItemFactoryParams(event = refEvent))
val informationData = messageInformationDataFactory.create(params)
- val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
+ val attributes = messageItemAttributesFactory.create(null, informationData, params.callback, params.reactionsSummaryEvents)
when (event.root.getClearType()) {
EventType.KEY_VERIFICATION_CANCEL -> {
@@ -100,7 +100,8 @@ class VerificationItemFactory @Inject constructor(
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
- readReceiptsCallback = attributes.readReceiptsCallback
+ readReceiptsCallback = attributes.readReceiptsCallback,
+ reactionsSummaryEvents = attributes.reactionsSummaryEvents
)
)
.highlighted(params.isHighlighted)
@@ -133,7 +134,8 @@ class VerificationItemFactory @Inject constructor(
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
- readReceiptsCallback = attributes.readReceiptsCallback
+ readReceiptsCallback = attributes.readReceiptsCallback,
+ reactionsSummaryEvents = attributes.reactionsSummaryEvents
)
)
.highlighted(params.isHighlighted)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
index a08383315c..647b34c626 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
@@ -88,7 +88,8 @@ class WidgetItemFactory @Inject constructor(
userOfInterest = userOfInterest,
callback = params.callback,
isStillActive = isCallStillActive,
- formattedDuration = ""
+ formattedDuration = "",
+ reactionsSummaryEvents = params.reactionsSummaryEvents
)
return CallTileTimelineItem_()
.attributes(attributes)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt
index 0cf30c8c01..7262284c95 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt
@@ -19,7 +19,9 @@ package im.vector.app.features.home.room.detail.timeline.helper
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
+import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import im.vector.app.R
@@ -37,7 +39,8 @@ class LocationPinProvider @Inject constructor(
private val context: Context,
private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter,
- private val avatarRenderer: AvatarRenderer
+ private val avatarRenderer: AvatarRenderer,
+ private val matrixItemColorProvider: MatrixItemColorProvider
) {
private val cache = mutableMapOf()
@@ -61,35 +64,42 @@ class LocationPinProvider @Inject constructor(
return
}
- activeSessionHolder.getActiveSession().getUser(userId)?.toMatrixItem()?.let {
- val size = dimensionConverter.dpToPx(44)
- avatarRenderer.render(glideRequests, it, object : CustomTarget(size, size) {
- override fun onResourceReady(resource: Drawable, transition: Transition?) {
- Timber.d("## Location: onResourceReady")
- val pinDrawable = createPinDrawable(resource)
- cache[userId] = pinDrawable
- callback(pinDrawable)
- }
+ activeSessionHolder
+ .getActiveSession()
+ .getUser(userId)
+ ?.toMatrixItem()
+ ?.let { userItem ->
+ val size = dimensionConverter.dpToPx(44)
+ val bgTintColor = matrixItemColorProvider.getColor(userItem)
+ avatarRenderer.render(glideRequests, userItem, object : CustomTarget(size, size) {
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ Timber.d("## Location: onResourceReady")
+ val pinDrawable = createPinDrawable(resource, bgTintColor)
+ cache[userId] = pinDrawable
+ callback(pinDrawable)
+ }
- override fun onLoadCleared(placeholder: Drawable?) {
- // Is it possible? Put placeholder instead?
- // FIXME The doc says it has to be implemented and should free resources
- Timber.d("## Location: onLoadCleared")
- }
+ override fun onLoadCleared(placeholder: Drawable?) {
+ // Is it possible? Put placeholder instead?
+ // FIXME The doc says it has to be implemented and should free resources
+ Timber.d("## Location: onLoadCleared")
+ }
- override fun onLoadFailed(errorDrawable: Drawable?) {
- Timber.w("## Location: onLoadFailed")
- errorDrawable ?: return
- val pinDrawable = createPinDrawable(errorDrawable)
- cache[userId] = pinDrawable
- callback(pinDrawable)
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ Timber.w("## Location: onLoadFailed")
+ errorDrawable ?: return
+ val pinDrawable = createPinDrawable(errorDrawable, bgTintColor)
+ cache[userId] = pinDrawable
+ callback(pinDrawable)
+ }
+ })
}
- })
- }
}
- private fun createPinDrawable(drawable: Drawable): Drawable {
+ private fun createPinDrawable(drawable: Drawable, @ColorInt bgTintColor: Int): Drawable {
val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!!
+ // use mutate on drawable to avoid sharing the color when we have multiple different user pins
+ DrawableCompat.setTint(bgUserPin.mutate(), bgTintColor)
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, drawable))
val horizontalInset = dimensionConverter.dpToPx(4)
val topInset = dimensionConverter.dpToPx(4)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 59b39d17ef..97b3a8f445 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -93,7 +93,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout,
- reactionsSummary = reactionsSummaryFactory.create(event, params.callback),
+ reactionsSummary = reactionsSummaryFactory.create(event),
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
PollResponseData(
myVote = it.aggregatedContent?.myVote,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
index 845b765101..426561054b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
@@ -24,6 +24,7 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
+import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import javax.inject.Inject
@@ -38,6 +39,7 @@ class MessageItemAttributesFactory @Inject constructor(
fun create(messageContent: Any?,
informationData: MessageInformationData,
callback: TimelineEventController.Callback?,
+ reactionsSummaryEvents: ReactionsSummaryEvents?,
threadDetails: ThreadDetails? = null): AbsMessageItem.Attributes {
return AbsMessageItem.Attributes(
avatarSize = avatarSizeProvider.avatarSize,
@@ -60,6 +62,7 @@ class MessageItemAttributesFactory @Inject constructor(
emojiTypeFace = emojiCompatFontProvider.typeface,
decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message),
threadDetails = threadDetails,
+ reactionsSummaryEvents = reactionsSummaryEvents,
areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled()
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ReactionsSummaryFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ReactionsSummaryFactory.kt
index fcc98ff729..3ba5997ee3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ReactionsSummaryFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ReactionsSummaryFactory.kt
@@ -34,7 +34,7 @@ class ReactionsSummaryFactory @Inject constructor() {
return eventsRequestingBuild.remove(event.eventId)
}
- fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): ReactionsSummaryData {
+ fun create(event: TimelineEvent): ReactionsSummaryData {
val eventId = event.eventId
val showAllStates = showAllReactionsByEvent.contains(eventId)
val reactions = event.annotations?.reactionsSummary
@@ -43,21 +43,24 @@ class ReactionsSummaryFactory @Inject constructor() {
}
return ReactionsSummaryData(
reactions = reactions,
- showAll = showAllStates,
- onShowMoreClicked = {
- showAllReactionsByEvent.add(eventId)
- onRequestBuild(eventId)
- },
- onShowLessClicked = {
- showAllReactionsByEvent.remove(eventId)
- onRequestBuild(eventId)
- },
- onAddMoreClicked = {
- callback?.onAddMoreReaction(event)
- }
+ showAll = showAllStates
)
}
+ fun onAddMoreClicked(callback: TimelineEventController.Callback?, event: TimelineEvent) {
+ callback?.onAddMoreReaction(event)
+ }
+
+ fun onShowMoreClicked(eventId: String) {
+ showAllReactionsByEvent.add(eventId)
+ onRequestBuild(eventId)
+ }
+
+ fun onShowLessClicked(eventId: String) {
+ showAllReactionsByEvent.remove(eventId)
+ onRequestBuild(eventId)
+ }
+
private fun onRequestBuild(eventId: String) {
eventsRequestingBuild.add(eventId)
onRequestBuild?.invoke()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
index 430e0970bb..4f08c9d05f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
@@ -121,18 +121,24 @@ abstract class AbsBaseMessageItem : BaseEventItem
val showReactionsTextView = createReactionTextView(holder)
if (reactionsSummary.showAll) {
showReactionsTextView.setText(R.string.message_reaction_show_less)
- showReactionsTextView.onClick { reactionsSummary.onShowLessClicked() }
+ showReactionsTextView.onClick {
+ baseAttributes.reactionsSummaryEvents?.onShowLessClicked?.invoke()
+ }
} else {
val moreCount = reactions.count() - MAX_REACTIONS_TO_SHOW
showReactionsTextView.text = holder.view.resources.getQuantityString(R.plurals.message_reaction_show_more, moreCount, moreCount)
- showReactionsTextView.onClick { reactionsSummary.onShowMoreClicked() }
+ showReactionsTextView.onClick {
+ baseAttributes.reactionsSummaryEvents?.onShowMoreClicked?.invoke()
+ }
}
holder.reactionsContainer.addView(showReactionsTextView)
}
val addMoreReactionsTextView = createReactionTextView(holder)
addMoreReactionsTextView.text = holder.view.context.getDrawableAsSpannable(R.drawable.ic_add_reaction_small)
- addMoreReactionsTextView.onClick { reactionsSummary.onAddMoreClicked() }
+ addMoreReactionsTextView.onClick {
+ baseAttributes.reactionsSummaryEvents?.onAddMoreClicked?.invoke()
+ }
holder.reactionsContainer.addView(addMoreReactionsTextView)
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
@@ -180,6 +186,7 @@ abstract class AbsBaseMessageItem : BaseEventItem
// val memberClickListener: ClickListener?
val reactionPillCallback: TimelineEventController.ReactionPillCallback?
+ val reactionsSummaryEvents: ReactionsSummaryEvents?
// val avatarCallback: TimelineEventController.AvatarCallback?
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 9e8f86c26e..2fac9df665 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -105,6 +105,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
} else {
holder.timeView.isVisible = false
}
+
// Render send state indicator
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
@@ -184,7 +185,8 @@ abstract class AbsMessageItem : AbsBaseMessageItem
val emojiTypeFace: Typeface? = null,
val decryptionErrorMessage: String? = null,
val threadDetails: ThreadDetails? = null,
- val areThreadMessagesEnabled: Boolean = false
+ val areThreadMessagesEnabled: Boolean = false,
+ override val reactionsSummaryEvents: ReactionsSummaryEvents? = null,
) : AbsBaseMessageItem.Attributes {
// Have to override as it's used to diff epoxy items
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
index 6db0b0c380..ea130901b1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
@@ -263,7 +263,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem? = null,
- val showAll: Boolean = false,
+ val showAll: Boolean = false
+) : Parcelable
+
+data class ReactionsSummaryEvents(
val onShowMoreClicked: () -> Unit,
val onShowLessClicked: () -> Unit,
val onAddMoreClicked: () -> Unit
-) : Parcelable
+)
@Parcelize
data class ReactionInfoData(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt
index fdde087b44..2d9119f14c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt
@@ -93,7 +93,8 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
@@ -141,22 +173,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
}
}
+ private fun TimelineMessageLayout.Bubble.setAdditionalTopSpace() = apply {
+ views.additionalTopSpace.isVisible = addTopMargin
+ }
+
private fun TimelineMessageLayout.Bubble.CornersRadius.toFloatArray(): FloatArray {
return floatArrayOf(topStartRadius, topStartRadius, topEndRadius, topEndRadius, bottomEndRadius, bottomEndRadius, bottomStartRadius, bottomStartRadius)
}
-
- private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) {
- val shapeAppearanceModel = messageLayout.cornersRadius.shapeAppearanceModel()
- bubbleDrawable.apply {
- this.shapeAppearanceModel = shapeAppearanceModel
- this.fillColor = if (messageLayout.isPseudoBubble) {
- ColorStateList.valueOf(Color.TRANSPARENT)
- } else {
- val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound
- val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr)
- ColorStateList.valueOf(backgroundColor)
- }
- }
- rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt
index bec3ccc643..6057072e41 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt
@@ -33,6 +33,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class RoomCategoryItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var title: String
+ @EpoxyAttribute var itemCount: Int = 0
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@@ -46,14 +47,16 @@ abstract class RoomCategoryItem : VectorEpoxyModel() {
DrawableCompat.setTint(it, tintColor)
}
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
- holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
holder.titleView.text = title
+ holder.counterView.text = itemCount.takeIf { it > 0 }?.toString().orEmpty()
+ holder.counterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
holder.rootView.onClick(listener)
}
class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind(R.id.roomCategoryUnreadCounterBadgeView)
val titleView by bind(R.id.roomCategoryTitleView)
+ val counterView by bind(R.id.roomCategoryCounterView)
val rootView by bind(R.id.roomCategoryRootView)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
index 28849204c4..4265eebe62 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
@@ -23,6 +23,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
@@ -50,8 +52,10 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -287,6 +291,7 @@ class RoomListFragment @Inject constructor(
))
checkEmptyState()
}
+ observeItemCount(section, sectionAdapter)
section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
notificationCount = counts.totalCount,
@@ -310,6 +315,7 @@ class RoomListFragment @Inject constructor(
))
checkEmptyState()
}
+ observeItemCount(section, sectionAdapter)
section.isExpanded.observe(viewLifecycleOwner) { _ ->
refreshCollapseStates()
}
@@ -326,6 +332,7 @@ class RoomListFragment @Inject constructor(
isLoading = false))
checkEmptyState()
}
+ observeItemCount(section, sectionAdapter)
section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
notificationCount = counts.totalCount,
@@ -373,6 +380,18 @@ class RoomListFragment @Inject constructor(
}
}
+ private fun observeItemCount(section: RoomsSection, sectionAdapter: SectionHeaderAdapter) {
+ lifecycleScope.launch {
+ section.itemCount
+ .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
+ .collect { count ->
+ sectionAdapter.updateSection(
+ sectionAdapter.roomsSectionData.copy(itemCount = count)
+ )
+ }
+ }
+ }
+
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
index 77f61149f8..ec7915ba34 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt
@@ -28,6 +28,7 @@ import im.vector.app.features.invite.showInvites
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -72,7 +73,18 @@ class RoomListSectionBuilderGroup(
session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
onUpdatable(updatableFilterLivePageResult)
- sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
+
+ val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
+ .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
+ .distinctUntilChanged()
+
+ sections.add(
+ RoomsSection(
+ sectionName = name,
+ livePages = updatableFilterLivePageResult.livePagedList,
+ itemCount = itemCountFlow
+ )
+ )
}
}
)
@@ -109,9 +121,7 @@ class RoomListSectionBuilderGroup(
.onEach { groupingMethod ->
val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
activeGroupAwareQueries.onEach { updater ->
- updater.updateQuery { query ->
- query.copy(activeGroupId = selectedGroupId)
- }
+ updater.queryParams = updater.queryParams.copy(activeGroupId = selectedGroupId)
}
}.launchIn(coroutineScope)
@@ -265,7 +275,8 @@ class RoomListSectionBuilderGroup(
RoomsSection(
sectionName = name,
livePages = livePagedList,
- notifyOfLocalEcho = notifyOfLocalEcho
+ notifyOfLocalEcho = notifyOfLocalEcho,
+ itemCount = session.getRoomCountFlow(roomQueryParams)
)
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
index 296e61690b..f82dbd43e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
@@ -91,7 +92,18 @@ class RoomListSectionBuilderSpace(
session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
onUpdatable(updatableFilterLivePageResult)
- sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
+
+ val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
+ .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
+ .distinctUntilChanged()
+
+ sections.add(
+ RoomsSection(
+ sectionName = name,
+ livePages = updatableFilterLivePageResult.livePagedList,
+ itemCount = itemCountFlow
+ )
+ )
}
}
)
@@ -261,7 +273,8 @@ class RoomListSectionBuilderSpace(
RoomsSection(
sectionName = stringProvider.getString(R.string.suggested_header),
liveSuggested = liveSuggestedRooms,
- notifyOfLocalEcho = false
+ notifyOfLocalEcho = false,
+ itemCount = suggestedRoomsFlow.map { suggestions -> suggestions.size }
)
)
}
@@ -338,11 +351,9 @@ class RoomListSectionBuilderSpace(
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
- it.updateQuery {
- it.copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
- )
- }
+ it.queryParams = roomQueryParams.copy(
+ activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
+ )
}
})
}
@@ -350,17 +361,13 @@ class RoomListSectionBuilderSpace(
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
if (roomId != null) {
- it.updateQuery {
- it.copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
- )
- }
+ it.queryParams = roomQueryParams.copy(
+ activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
+ )
} else {
- it.updateQuery {
- it.copy(
- activeSpaceFilter = ActiveSpaceFilter.None
- )
- }
+ it.queryParams = roomQueryParams.copy(
+ activeSpaceFilter = ActiveSpaceFilter.None
+ )
}
}
})
@@ -390,11 +397,19 @@ class RoomListSectionBuilderSpace(
.flowOn(Dispatchers.Default)
.launchIn(viewModelScope)
+ val itemCountFlow = livePagedList.asFlow()
+ .flatMapLatest {
+ val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
+ session.getRoomCountFlow(queryParams)
+ }
+ .distinctUntilChanged()
+
sections.add(
RoomsSection(
sectionName = name,
livePages = livePagedList,
- notifyOfLocalEcho = notifyOfLocalEcho
+ notifyOfLocalEcho = notifyOfLocalEcho,
+ itemCount = itemCountFlow
)
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
index 4a81a8b526..ec8b01876b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
@@ -192,8 +192,8 @@ class RoomListViewModel @AssistedInject constructor(
roomFilter = action.filter
)
}
- updatableQuery?.updateQuery {
- it.copy(
+ updatableQuery?.apply {
+ queryParams = queryParams.copy(
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.NORMALIZED)
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt
index 5eaae262a6..357df5ecd3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedList
+import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
@@ -29,6 +30,7 @@ data class RoomsSection(
val liveList: LiveData>? = null,
val liveSuggested: LiveData? = null,
val isExpanded: MutableLiveData = MutableLiveData(true),
+ val itemCount: Flow,
val notificationCount: MutableLiveData = MutableLiveData(RoomAggregateNotificationCount(0, 0)),
val notifyOfLocalEcho: Boolean = false
)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
index 560e0d00a3..2e6436d21d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt
@@ -33,6 +33,7 @@ class SectionHeaderAdapter constructor(
data class RoomsSectionData(
val name: String,
+ val itemCount: Int = 0,
val isExpanded: Boolean = true,
val notificationCount: Int = 0,
val isHighlighted: Boolean = false,
@@ -85,8 +86,9 @@ class SectionHeaderAdapter constructor(
val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
+ binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
+ binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty()
binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted))
- binding.roomCategoryTitleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
index a7e93a3f6c..b1033f2797 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
@@ -30,6 +30,10 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentLocationSharingBinding
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
+import im.vector.app.features.location.option.LocationSharingOption
+import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference
import javax.inject.Inject
@@ -37,7 +41,9 @@ import javax.inject.Inject
* We should consider using SupportMapFragment for a out of the box lifecycle handling
*/
class LocationSharingFragment @Inject constructor(
- private val urlMapProvider: UrlMapProvider
+ private val urlMapProvider: UrlMapProvider,
+ private val avatarRenderer: AvatarRenderer,
+ private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorBaseFragment() {
private val viewModel: LocationSharingViewModel by fragmentViewModel()
@@ -45,6 +51,8 @@ class LocationSharingFragment @Inject constructor(
// Keep a ref to handle properly the onDestroy callback
private var mapView: WeakReference? = null
+ private var hasRenderedUserAvatar = false
+
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
return FragmentLocationSharingBinding.inflate(inflater, container, false)
}
@@ -59,9 +67,7 @@ class LocationSharingFragment @Inject constructor(
views.mapView.initialize(urlMapProvider.getMapUrl())
}
- views.shareLocationContainer.debouncedClicks {
- viewModel.handle(LocationSharingAction.OnShareLocation)
- }
+ initOptionsPicker()
viewModel.observeViewEvents {
when (it) {
@@ -107,6 +113,12 @@ class LocationSharingFragment @Inject constructor(
super.onDestroy()
}
+ override fun invalidate() = withState(viewModel) { state ->
+ views.mapView.render(state.toMapState())
+ views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
+ updateUserAvatar(state.userItem)
+ }
+
private fun handleLocationNotAvailableError() {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.location_not_available_dialog_title)
@@ -118,8 +130,28 @@ class LocationSharingFragment @Inject constructor(
.show()
}
- override fun invalidate() = withState(viewModel) { state ->
- views.mapView.render(state.toMapState())
- views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
+ private fun initOptionsPicker() {
+ // TODO
+ // change the options dynamically depending on the current chosen location
+ views.shareLocationOptionsPicker.render(LocationSharingOption.USER_CURRENT)
+ views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
+ // TODO
+ }
+ views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
+ viewModel.handle(LocationSharingAction.OnShareLocation)
+ }
+ views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
+ // TODO
+ }
+ }
+
+ private fun updateUserAvatar(userItem: MatrixItem.UserItem?) {
+ userItem?.takeUnless { hasRenderedUserAvatar }
+ ?.let {
+ hasRenderedUserAvatar = true
+ avatarRenderer.render(it, views.shareLocationOptionsPicker.optionUserCurrent.iconView)
+ val tintColor = matrixItemColorProvider.getColor(it)
+ views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
index f4e1fd0281..989ec255e5 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
@@ -26,6 +26,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.util.toMatrixItem
class LocationSharingViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationSharingViewState,
@@ -45,9 +46,14 @@ class LocationSharingViewModel @AssistedInject constructor(
init {
locationTracker.start(this)
+ setUserItem()
createPin()
}
+ private fun setUserItem() {
+ setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
+ }
+
private fun createPin() {
locationPinProvider.create(session.myUserId) {
setState {
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
index a9a24094eb..e63206f515 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
@@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import com.airbnb.mvrx.MavericksState
import im.vector.app.R
+import org.matrix.android.sdk.api.util.MatrixItem
enum class LocationSharingMode(@StringRes val titleRes: Int) {
STATIC_SHARING(R.string.location_activity_title_static_sharing),
@@ -29,6 +30,7 @@ enum class LocationSharingMode(@StringRes val titleRes: Int) {
data class LocationSharingViewState(
val roomId: String,
val mode: LocationSharingMode,
+ val userItem: MatrixItem.UserItem? = null,
val lastKnownLocation: LocationData? = null,
val pinDrawable: Drawable? = null
) : MavericksState {
diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOption.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOption.kt
new file mode 100644
index 0000000000..ebf9bde5f6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOption.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.location.option
+
+enum class LocationSharingOption {
+ /**
+ * Current user's location.
+ */
+ USER_CURRENT,
+
+ /**
+ * User's location during a certain amount of time.
+ */
+ USER_LIVE,
+
+ /**
+ * Static location pinned by the user.
+ */
+ PINNED
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt
new file mode 100644
index 0000000000..1aea1ff613
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.location.option
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import im.vector.app.R
+import im.vector.app.databinding.ViewLocationSharingOptionPickerBinding
+
+/**
+ * Custom view to display the location sharing option picker.
+ */
+class LocationSharingOptionPickerView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ val optionPinned: LocationSharingOptionView
+ get() = binding.locationSharingOptionPinned
+
+ val optionUserCurrent: LocationSharingOptionView
+ get() = binding.locationSharingOptionUserCurrent
+
+ val optionUserLive: LocationSharingOptionView
+ get() = binding.locationSharingOptionUserLive
+
+ private val divider1: View
+ get() = binding.locationSharingOptionsDivider1
+
+ private val divider2: View
+ get() = binding.locationSharingOptionsDivider2
+
+ private val binding = ViewLocationSharingOptionPickerBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+
+ init {
+ applyBackground()
+ }
+
+ fun render(vararg options: LocationSharingOption) {
+ val optionsNumber = options.toSet().size
+ val isPinnedVisible = options.contains(LocationSharingOption.PINNED)
+ val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT)
+ val isUserLiveVisible = options.contains(LocationSharingOption.USER_LIVE)
+
+ optionPinned.isVisible = isPinnedVisible
+ divider1.isVisible = isPinnedVisible && optionsNumber > 1
+ optionUserCurrent.isVisible = isUserCurrentVisible
+ divider2.isVisible = isUserCurrentVisible && isUserLiveVisible
+ optionUserLive.isVisible = isUserLiveVisible
+ }
+
+ private fun applyBackground() {
+ val outValue = TypedValue()
+ context.theme.resolveAttribute(
+ R.attr.colorSurface,
+ outValue,
+ true
+ )
+ binding.root.background = ContextCompat.getDrawable(
+ context,
+ outValue.resourceId
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt
new file mode 100644
index 0000000000..d11ff00261
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionView.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.location.option
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.setPadding
+import im.vector.app.R
+import im.vector.app.core.extensions.tintBackground
+import im.vector.app.databinding.ViewLocationSharingOptionBinding
+
+/**
+ * Custom view to display a location sharing option.
+ */
+class LocationSharingOptionView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ val iconView: ImageView
+ get() = binding.shareLocationOptionIcon
+
+ private val binding = ViewLocationSharingOptionBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+
+ init {
+ context.theme.obtainStyledAttributes(
+ attrs,
+ R.styleable.LocationSharingOptionView,
+ 0,
+ 0
+ ).run {
+ try {
+ setIcon(this)
+ setTitle(this)
+ } finally {
+ recycle()
+ }
+ }
+ }
+
+ fun setIconBackgroundTint(@ColorInt color: Int) {
+ binding.shareLocationOptionIcon.tintBackground(color)
+ }
+
+ private fun setIcon(typedArray: TypedArray) {
+ val icon = typedArray.getDrawable(R.styleable.LocationSharingOptionView_locShareIcon)
+ val background = typedArray.getDrawable(R.styleable.LocationSharingOptionView_locShareIconBackground)
+ val backgroundTint = typedArray.getColor(
+ R.styleable.LocationSharingOptionView_locShareIconBackgroundTint,
+ ContextCompat.getColor(context, android.R.color.transparent)
+ )
+ val padding = typedArray.getDimensionPixelOffset(
+ R.styleable.LocationSharingOptionView_locShareIconPadding,
+ context.resources.getDimensionPixelOffset(R.dimen.location_sharing_option_default_padding)
+ )
+ val description = typedArray.getString(R.styleable.LocationSharingOptionView_locShareIconDescription)
+
+ iconView.setImageDrawable(icon)
+ iconView.background = background
+ iconView.tintBackground(backgroundTint)
+ iconView.setPadding(padding)
+ iconView.contentDescription = description
+ }
+
+ private fun setTitle(typedArray: TypedArray) {
+ val title = typedArray.getString(R.styleable.LocationSharingOptionView_locShareTitle)
+ binding.shareLocationOptionTitle.text = title
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
index ec034173fc..3cdc9e8c76 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
@@ -190,7 +190,7 @@ class NotifiableEventResolver @Inject constructor(
}
}
- private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) {
+ private suspend fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) {
if (root.isEncrypted() && root.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
index b35c110892..4f16231747 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
@@ -75,6 +75,7 @@ sealed class OnboardingAction : VectorViewModelAction {
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction()
+ object PersonalizeProfile : OnboardingAction()
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
object UpdateDisplayNameSkipped : OnboardingAction()
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
index 8a09879b15..82ee48411d 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
@@ -51,9 +51,8 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OnAccountCreated : OnboardingViewEvents()
object OnAccountSignedIn : OnboardingViewEvents()
object OnTakeMeHome : OnboardingViewEvents()
- object OnPersonalizeProfile : OnboardingViewEvents()
- object OnDisplayNameUpdated : OnboardingViewEvents()
- object OnDisplayNameSkipped : OnboardingViewEvents()
+ object OnChooseDisplayName : OnboardingViewEvents()
+ object OnChooseProfilePicture : OnboardingViewEvents()
object OnPersonalizationComplete : OnboardingViewEvents()
object OnBack : OnboardingViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 413745f98c..36020fbe61 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -48,6 +48,7 @@ import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
import org.matrix.android.sdk.api.auth.AuthenticationService
@@ -156,12 +157,13 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.ResetAction -> handleResetAction(action)
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
- is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName)
- OnboardingAction.UpdateDisplayNameSkipped -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped)
- OnboardingAction.UpdateProfilePictureSkipped -> _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
+ OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete()
+ OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization()
+ OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile()
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
+ is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
}.exhaustive
}
@@ -762,15 +764,33 @@ class OnboardingViewModel @AssistedInject constructor(
authenticationService.reset()
session.configureAndStart(applicationContext)
- setState {
- copy(
- asyncLoginAction = Success(Unit)
- )
- }
when (isAccountCreated) {
- true -> _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
- false -> _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
+ true -> {
+ val personalizationState = createPersonalizationState(session, state)
+ setState {
+ copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState)
+ }
+ _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
+ }
+ false -> {
+ setState { copy(asyncLoginAction = Success(Unit)) }
+ _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
+ }
+ }
+ }
+
+ private suspend fun createPersonalizationState(session: Session, state: OnboardingViewState): PersonalizationState {
+ return when {
+ vectorFeatures.isOnboardingPersonalizeEnabled() -> {
+ val homeServerCapabilities = session.getHomeServerCapabilities()
+ val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull()
+ state.personalizationState.copy(
+ supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName,
+ supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar
+ )
+ }
+ else -> state.personalizationState
}
}
@@ -910,7 +930,7 @@ class OnboardingViewModel @AssistedInject constructor(
personalizationState = personalizationState.copy(displayName = displayName)
)
}
- _viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated)
+ handleDisplayNameStepComplete()
} catch (error: Throwable) {
setState { copy(asyncDisplayName = Fail(error)) }
_viewEvents.post(OnboardingViewEvents.Failure(error))
@@ -918,12 +938,37 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
+ private fun handlePersonalizeProfile() {
+ withPersonalisationState {
+ when {
+ it.supportsChangingDisplayName -> _viewEvents.post(OnboardingViewEvents.OnChooseDisplayName)
+ it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture)
+ else -> {
+ throw IllegalStateException("It should not be possible to personalize without supporting display name or avatar changing")
+ }
+ }
+ }
+ }
+
+ private fun handleDisplayNameStepComplete() {
+ withPersonalisationState {
+ when {
+ it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture)
+ else -> completePersonalization()
+ }
+ }
+ }
+
private fun handleProfilePictureSelected(action: OnboardingAction.ProfilePictureSelected) {
setState {
copy(personalizationState = personalizationState.copy(selectedPictureUri = action.uri))
}
}
+ private fun withPersonalisationState(block: (PersonalizationState) -> Unit) {
+ withState { block(it.personalizationState) }
+ }
+
private fun updateProfilePicture() {
withState { state ->
when (val pictureUri = state.personalizationState.selectedPictureUri) {
@@ -955,6 +1000,10 @@ class OnboardingViewModel @AssistedInject constructor(
}
private fun onProfilePictureSaved() {
+ completePersonalization()
+ }
+
+ private fun completePersonalization() {
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
index bd5d93ae4d..8747de6da8 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
@@ -22,7 +22,6 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.PersistState
-import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ServerType
@@ -83,10 +82,6 @@ data class OnboardingViewState(
asyncDisplayName is Loading ||
asyncProfilePicture is Loading
}
-
- fun isAuthTaskCompleted(): Boolean {
- return asyncLoginAction is Success
- }
}
enum class OnboardingFlow {
@@ -97,6 +92,11 @@ enum class OnboardingFlow {
@Parcelize
data class PersonalizationState(
+ val supportsChangingDisplayName: Boolean = false,
+ val supportsChangingProfilePicture: Boolean = false,
val displayName: String? = null,
val selectedPictureUri: Uri? = null
-) : Parcelable
+) : Parcelable {
+
+ fun supportsPersonalization() = supportsChangingDisplayName || supportsChangingProfilePicture
+}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
index d021fd2813..ccfb863a5b 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
@@ -20,11 +20,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
+import im.vector.app.features.onboarding.OnboardingViewState
import javax.inject.Inject
class FtueAuthAccountCreatedFragment @Inject constructor(
@@ -42,8 +44,15 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
private fun setupViews() {
views.accountCreatedSubtitle.text = getString(R.string.ftue_account_created_subtitle, activeSessionHolder.getActiveSession().myUserId)
- views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnPersonalizeProfile)) }
+ views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) }
views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
+ views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
+ }
+
+ override fun updateWithState(state: OnboardingViewState) {
+ val canPersonalize = state.personalizationState.supportsPersonalization()
+ views.personalizeButtonGroup.isVisible = canPersonalize
+ views.takeMeHomeButtonGroup.isVisible = !canPersonalize
}
override fun resetViewModel() {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt
index bc1bf0c8bc..81300932db 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt
@@ -22,6 +22,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
+import androidx.core.view.isInvisible
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@@ -70,6 +71,8 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor(
}
override fun updateWithState(state: OnboardingViewState) {
+ views.profilePictureToolbar.isInvisible = !state.personalizationState.supportsChangingDisplayName
+
val hasSetPicture = state.personalizationState.selectedPictureUri != null
views.profilePictureSubmit.isEnabled = hasSetPicture
views.changeProfilePictureIcon.setImageResource(if (hasSetPicture) R.drawable.ic_edit else R.drawable.ic_camera_plain)
@@ -93,4 +96,14 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor(
override fun resetViewModel() {
// Nothing to do
}
+
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ return when (withState(viewModel) { it.personalizationState.supportsChangingDisplayName }) {
+ true -> super.onBackPressed(toolbarButton)
+ false -> {
+ viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
+ true
+ }
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index e336419e3f..2008726ac3 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -122,17 +122,9 @@ class FtueAuthVariant(
private fun updateWithState(viewState: OnboardingViewState) {
isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
- views.loginLoading.isVisible = shouldShowLoading(viewState)
+ views.loginLoading.isVisible = viewState.isLoading()
}
- private fun shouldShowLoading(viewState: OnboardingViewState) =
- if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
- viewState.isLoading()
- } else {
- // Keep loading when during success because of the delay when switching to the next Activity
- viewState.isLoading() || viewState.isAuthTaskCompleted()
- }
-
override fun setIsLoading(isLoading: Boolean) = Unit
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
@@ -230,12 +222,11 @@ class FtueAuthVariant(
FtueAuthUseCaseFragment::class.java,
option = commonOption)
}
- OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
+ is OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn()
- OnboardingViewEvents.OnPersonalizeProfile -> onPersonalizeProfile()
+ OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
- OnboardingViewEvents.OnDisplayNameUpdated -> onDisplayNameUpdated()
- OnboardingViewEvents.OnDisplayNameSkipped -> onDisplayNameUpdated()
+ OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true)
OnboardingViewEvents.OnBack -> activity.popBackstack()
}.exhaustive
@@ -399,15 +390,11 @@ class FtueAuthVariant(
}
private fun onAccountCreated() {
- if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
- activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
- activity.replaceFragment(
- views.loginFragmentContainer,
- FtueAuthAccountCreatedFragment::class.java,
- )
- } else {
- navigateToHome(createdAccount = true)
- }
+ activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ activity.replaceFragment(
+ views.loginFragmentContainer,
+ FtueAuthAccountCreatedFragment::class.java
+ )
}
private fun navigateToHome(createdAccount: Boolean) {
@@ -416,14 +403,14 @@ class FtueAuthVariant(
activity.finish()
}
- private fun onPersonalizeProfile() {
+ private fun onChooseDisplayName() {
activity.addFragmentToBackstack(views.loginFragmentContainer,
FtueAuthChooseDisplayNameFragment::class.java,
option = commonOption
)
}
- private fun onDisplayNameUpdated() {
+ private fun onChooseProfilePicture() {
activity.addFragmentToBackstack(views.loginFragmentContainer,
FtueAuthChooseProfilePictureFragment::class.java,
option = commonOption
diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt
index 3164daf634..c2d63aa8d3 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt
@@ -94,6 +94,12 @@ class AddRoomListController @Inject constructor(
}
var totalSize: Int = 0
+ set(value) {
+ if (value != field) {
+ field = value
+ requestForcedModelBuild()
+ }
+ }
var selectedItems: Map = emptyMap()
set(value) {
@@ -120,7 +126,8 @@ class AddRoomListController @Inject constructor(
add(
RoomCategoryItem_().apply {
id("header")
- title(host.sectionName ?: "")
+ title(host.sectionName.orEmpty())
+ itemCount(host.totalSize)
expanded(host.expanded)
listener {
host.expanded = !host.expanded
diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt
index bcf0a8a949..8d6a351013 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt
@@ -22,6 +22,8 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
@@ -35,9 +37,12 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSpaceAddRoomsBinding
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import reactivecircus.flowbinding.appcompat.queryTextChanges
import javax.inject.Inject
@@ -169,48 +174,63 @@ class SpaceAddRoomFragment @Inject constructor(
}
private fun setupRecyclerView() {
- val concatAdapter = ConcatAdapter()
- spaceEpoxyController.sectionName = getString(R.string.spaces_header)
- roomEpoxyController.sectionName = getString(R.string.rooms_header)
- spaceEpoxyController.listener = this
- roomEpoxyController.listener = this
+ setupSpaceSection()
+ setupRoomSection()
+ setupDmSection()
- viewModel.updatableLiveSpacePageResult.liveBoundaries.observe(viewLifecycleOwner) {
+ views.roomList.adapter = ConcatAdapter().apply {
+ addAdapter(roomEpoxyController.adapter)
+ addAdapter(spaceEpoxyController.adapter)
+ addAdapter(dmEpoxyController.adapter)
+ }
+ }
+
+ private fun setupSpaceSection() {
+ spaceEpoxyController.sectionName = getString(R.string.spaces_header)
+ spaceEpoxyController.listener = this
+ viewModel.spaceUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
spaceEpoxyController.boundaryChange(it)
}
- viewModel.updatableLiveSpacePageResult.livePagedList.observe(viewLifecycleOwner) {
- spaceEpoxyController.totalSize = it.size
+ viewModel.spaceUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
spaceEpoxyController.submitList(it)
}
+ listenItemCount(viewModel.spaceCountFlow) { spaceEpoxyController.totalSize = it }
+ }
- viewModel.updatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
+ private fun setupRoomSection() {
+ roomEpoxyController.sectionName = getString(R.string.rooms_header)
+ roomEpoxyController.listener = this
+
+ viewModel.roomUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
roomEpoxyController.boundaryChange(it)
}
- viewModel.updatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
- roomEpoxyController.totalSize = it.size
+ viewModel.roomUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
roomEpoxyController.submitList(it)
}
-
+ listenItemCount(viewModel.roomCountFlow) { roomEpoxyController.totalSize = it }
views.roomList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
views.roomList.setHasFixedSize(true)
+ }
- concatAdapter.addAdapter(roomEpoxyController.adapter)
- concatAdapter.addAdapter(spaceEpoxyController.adapter)
-
+ private fun setupDmSection() {
// This controller can be disabled depending on the space type (public or not)
- viewModel.updatableDMLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
- dmEpoxyController.boundaryChange(it)
- }
- viewModel.updatableDMLivePageResult.livePagedList.observe(viewLifecycleOwner) {
- dmEpoxyController.totalSize = it.size
- dmEpoxyController.submitList(it)
- }
dmEpoxyController.sectionName = getString(R.string.direct_chats_header)
dmEpoxyController.listener = this
+ viewModel.dmUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
+ dmEpoxyController.boundaryChange(it)
+ }
+ viewModel.dmUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
+ dmEpoxyController.submitList(it)
+ }
+ listenItemCount(viewModel.dmCountFlow) { dmEpoxyController.totalSize = it }
+ }
- concatAdapter.addAdapter(dmEpoxyController.adapter)
-
- views.roomList.adapter = concatAdapter
+ private fun listenItemCount(itemCountFlow: Flow, onEachAction: (Int) -> Unit) {
+ lifecycleScope.launch {
+ itemCountFlow
+ .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
+ .collect { count -> onEachAction(count) }
+ }
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt
index 8fa269d439..7d99c53f23 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt
@@ -17,7 +17,7 @@
package im.vector.app.features.spaces.manage
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.asFlow
import androidx.paging.PagedList
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
@@ -30,6 +30,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
@@ -60,7 +63,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
- val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy {
+ val spaceUpdatableLivePageResult: UpdatableLivePageResult by lazy {
session.getFilteredPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
@@ -79,7 +82,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
)
}
- val updatableLivePageResult: UpdatableLivePageResult by lazy {
+ val spaceCountFlow: Flow by lazy {
+ spaceUpdatableLivePageResult.livePagedList.asFlow()
+ .flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) }
+ .distinctUntilChanged()
+ }
+
+ val roomUpdatableLivePageResult: UpdatableLivePageResult by lazy {
session.getFilteredPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
@@ -99,7 +108,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
)
}
- val updatableDMLivePageResult: UpdatableLivePageResult by lazy {
+ val roomCountFlow: Flow by lazy {
+ roomUpdatableLivePageResult.livePagedList.asFlow()
+ .flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) }
+ .distinctUntilChanged()
+ }
+
+ val dmUpdatableLivePageResult: UpdatableLivePageResult by lazy {
session.getFilteredPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
@@ -119,6 +134,12 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
)
}
+ val dmCountFlow: Flow by lazy {
+ dmUpdatableLivePageResult.livePagedList.asFlow()
+ .flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) }
+ .distinctUntilChanged()
+ }
+
private val selectionList = mutableMapOf()
val selectionListLiveData = MutableLiveData