diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index a7daaac14b..1ab5d835b2 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -73,3 +73,14 @@ body:
- 'No'
validations:
required: true
+ - type: dropdown
+ id: pr
+ attributes:
+ label: Are you willing to provide a PR?
+ description: |
+ Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
+ options:
+ - 'Yes'
+ - 'No'
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
index 71adce718e..2dd968951f 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -34,3 +34,14 @@ body:
placeholder: Is there anything else you'd like to add?
validations:
required: false
+ - type: dropdown
+ id: pr
+ attributes:
+ label: Are you willing to provide a PR?
+ description: |
+ Don't worry, it's still OK to answer 'No' :).
+ options:
+ - 'Yes'
+ - 'No'
+ validations:
+ required: true
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index a97d532644..014139d0ba 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -5,6 +5,11 @@ on:
push:
branches: [ main, develop ]
+# Enrich gradle.properties for CI/CD
+env:
+ CI_GRADLE_ARG_PROPERTIES: >
+ -Porg.gradle.jvmargs=-Xmx4g
+
jobs:
check:
name: Project Check Suite
@@ -97,6 +102,25 @@ jobs:
comment_id: ${{ steps.fc.outputs.comment-id }}
})
+# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin
+ dependency-analysis:
+ name: Dependency analysis
+ runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('dep-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('dep-develop-{0}', github.sha) || format('dep-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v3
+ - name: Dependency analysis
+ run: ./gradlew buildHealth $CI_GRADLE_ARG_PROPERTIES
+ - name: Upload dependency analysis
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: dependency-analysis
+ path: build/reports/dependency-analysis/build-health-report.txt
+
# Lint for main module
android-lint:
name: Android Linter
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index 82d5931ce7..c49b33d582 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -9,15 +9,15 @@ jobs:
name: Add Z-Labs label for features behind labs flags
runs-on: ubuntu-latest
if: >
- contains(github.event.issue.labels.*.name, 'A-Maths') ||
- contains(github.event.issue.labels.*.name, 'A-Message-Pinning') ||
- contains(github.event.issue.labels.*.name, 'A-Polls') ||
- contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
- contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') ||
- contains(github.event.issue.labels.*.name, 'Z-IA') ||
- contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
- contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
- contains(github.event.issue.labels.*.name, 'A-Tags')
+ contains(github.event.issue.labels.*.name, 'A-Maths') ||
+ contains(github.event.issue.labels.*.name, 'A-Message-Pinning') ||
+ contains(github.event.issue.labels.*.name, 'A-Polls') ||
+ contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
+ contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') ||
+ contains(github.event.issue.labels.*.name, 'Z-IA') ||
+ contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
+ contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
+ contains(github.event.issue.labels.*.name, 'A-Tags')
steps:
- uses: actions/github-script@v5
with:
@@ -79,7 +79,7 @@ jobs:
name: X-Needs-Product to Design project board
runs-on: ubuntu-latest
if: >
- contains(github.event.issue.labels.*.name, 'X-Needs-Product')
+ contains(github.event.issue.labels.*.name, 'X-Needs-Product')
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
@@ -105,10 +105,7 @@ jobs:
# Skip in forks
if: >
github.repository == 'vector-im/element-android' &&
- (contains(github.event.issue.labels.*.name, 'A-Spaces') ||
- contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
- contains(github.event.issue.labels.*.name, 'A-Subspaces') ||
- contains(github.event.issue.labels.*.name, 'Z-IA'))
+ (contains(github.event.issue.labels.*.name, 'Team: Delight'))
steps:
- uses: octokit/graphql-action@v2.x
with:
@@ -201,7 +198,7 @@ jobs:
env:
PROJECT_ID: "PN_kwDOAM0swc3m-g"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
-
+
move_ftue_issues:
name: Z-FTUE to Mobile FTUE board
runs-on: ubuntu-latest
diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml
index 70c337e748..43e11edca5 100644
--- a/.github/workflows/triage-priority-bugs.yml
+++ b/.github/workflows/triage-priority-bugs.yml
@@ -15,9 +15,6 @@ jobs:
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
- !contains(github.event.issue.labels.*.name, 'A-Spaces') &&
- !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
- !contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
diff --git a/CHANGES.md b/CHANGES.md
index 4ff45e9c62..b0203c39e9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,67 @@
+Changes in Element v1.4.18 (2022-05-31)
+=======================================
+
+Features ✨
+----------
+ - Space explore screen changes: removed space card, added rooms filtering ([#5658](https://github.com/vector-im/element-android/issues/5658))
+ - Adds space or user id as a subtitle under rooms in search ([#5860](https://github.com/vector-im/element-android/issues/5860))
+ - Adds up navigation in spaces ([#6073](https://github.com/vector-im/element-android/issues/6073))
+ - Labs flag for enabling live location sharing ([#6098](https://github.com/vector-im/element-android/issues/6098))
+ - Added support for mandatory backup or passphrase from .well-known configuration. ([#6133](https://github.com/vector-im/element-android/issues/6133))
+ - Security - Asking for user confirmation when tapping URLs which contain unicode directional overrides ([#6163](https://github.com/vector-im/element-android/issues/6163))
+ - Add settings switch to allow autoplaying animated images ([#6166](https://github.com/vector-im/element-android/issues/6166))
+ - Live Location Sharing - User List Bottom Sheet ([#6170](https://github.com/vector-im/element-android/issues/6170))
+
+Bugfixes 🐛
+----------
+ - Fix some notifications not clearing when read ([#4862](https://github.com/vector-im/element-android/issues/4862))
+ - Do not switch away from home space on notification when "Show all Rooms in Home" is selected. ([#5827](https://github.com/vector-im/element-android/issues/5827))
+ - Use fixed text size in read receipt counter ([#5856](https://github.com/vector-im/element-android/issues/5856))
+ - Revert: Use member name instead of room name in DM creation item ([#6032](https://github.com/vector-im/element-android/issues/6032))
+ - Poll refactoring with unit tests ([#6074](https://github.com/vector-im/element-android/issues/6074))
+ - Correct .well-known/matrix/client handling for server_names which include ports. ([#6095](https://github.com/vector-im/element-android/issues/6095))
+ - Glide - Use current drawable while loading new static map image ([#6103](https://github.com/vector-im/element-android/issues/6103))
+ - Fix sending multiple invites to a room reaching only one or two people ([#6109](https://github.com/vector-im/element-android/issues/6109))
+ - Prevent widget web view from reloading on screen / orientation change ([#6140](https://github.com/vector-im/element-android/issues/6140))
+ - Fix decrypting redacted event from sending errors ([#6148](https://github.com/vector-im/element-android/issues/6148))
+ - Make widget web view request system permissions for camera and microphone (PSF-1061) ([#6149](https://github.com/vector-im/element-android/issues/6149))
+
+In development 🚧
+----------------
+ - Adds email input and verification screens to the new FTUE onboarding flow ([#5278](https://github.com/vector-im/element-android/issues/5278))
+ - FTUE - Adds the redesigned Sign In screen ([#5283](https://github.com/vector-im/element-android/issues/5283))
+ - [Live location sharing] Update message in timeline during the live ([#5689](https://github.com/vector-im/element-android/issues/5689))
+ - FTUE - Overrides sign up flow ordering for matrix.org only ([#5783](https://github.com/vector-im/element-android/issues/5783))
+ - Live location sharing: navigation from timeline to map screen
+ Live location sharing: show user pins on map screen ([#6012](https://github.com/vector-im/element-android/issues/6012))
+ - FTUE - Adds homeserver login/register deeplink support ([#6023](https://github.com/vector-im/element-android/issues/6023))
+ - [Live location sharing] Update entity in DB when a live is timed out ([#6123](https://github.com/vector-im/element-android/issues/6123))
+
+SDK API changes ⚠️
+------------------
+- Notifies other devices when a verification request sent from an Android device is accepted.` ([#5724](https://github.com/vector-im/element-android/issues/5724))
+- Some `val` have been changed to `fun` to increase their visibility in the generated documentation. Just add `()` if you were using them.
+- `KeysBackupService.state` has been replaced by `KeysBackupService.getState()`
+- `KeysBackupService.isStucked` has been replaced by `KeysBackupService.isStuck()`
+- SDK documentation improved ([#5952](https://github.com/vector-im/element-android/issues/5952))
+- Improve replay attacks and reduce duplicate message index errors ([#6077](https://github.com/vector-im/element-android/issues/6077))
+- Remove `RoomSummaryQueryParams.roomId`. If you need to observe a single room, use the new API `RoomService.getRoomSummaryLive(roomId: String)`
+- `ActiveSpaceFilter` has been renamed to `SpaceFilter`
+- `RoomCategoryFilter.ALL` has been removed, just pass `null` to not filter on Room category. ([#6143](https://github.com/vector-im/element-android/issues/6143))
+
+Other changes
+-------------
+ - leaving space experience changed to be aligned with iOS ([#5728](https://github.com/vector-im/element-android/issues/5728))
+ - @Ignore a number of tests that are currently failing in CI. ([#6025](https://github.com/vector-im/element-android/issues/6025))
+ - Remove ShortcutBadger lib and usage (it was dead code) ([#6041](https://github.com/vector-im/element-android/issues/6041))
+ - Test: Ensure calling 'fail()' is not caught by the catch block ([#6089](https://github.com/vector-im/element-android/issues/6089))
+ - Excludes transitive optional non FOSS google location dependency from fdroid builds ([#6100](https://github.com/vector-im/element-android/issues/6100))
+ - Fixed grammar errors in /vector/src/main/res/values/strings.xml ([#6132](https://github.com/vector-im/element-android/issues/6132))
+ - Downgrade gradle from 7.2.0 to 7.1.3 ([#6141](https://github.com/vector-im/element-android/issues/6141))
+ - Add Lao language to the in-app settings. ([#6196](https://github.com/vector-im/element-android/issues/6196))
+ - Remove the background location permission request ([#6198](https://github.com/vector-im/element-android/issues/6198))
+
+
Changes in Element v1.4.16 (2022-05-17)
=======================================
diff --git a/build.gradle b/build.gradle
index 25ff8b91c0..635db83e14 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,6 +41,9 @@ plugins {
id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.20.0"
+
+ // Dependency Analysis
+ id 'com.autonomousapps.dependency-analysis' version "1.4.0"
}
// https://github.com/jeremylong/DependencyCheck
@@ -219,3 +222,61 @@ project(":library:diff-match-patch") {
// }
// }
//}
+
+dependencyAnalysis {
+ dependencies {
+ bundle("kotlin-stdlib") {
+ includeGroup("org.jetbrains.kotlin")
+ }
+ bundle("react") {
+ includeGroup("com.facebook.react")
+ }
+ }
+ issues {
+ all {
+ ignoreKtx(true)
+ onUsedTransitiveDependencies {
+ // Transitively used dependencies that should be declared directly
+ severity("ignore")
+ }
+ onUnusedDependencies {
+ severity("fail")
+ }
+ onUnusedAnnotationProcessors {
+ severity("fail")
+ exclude("com.airbnb.android:epoxy-processor", "com.google.dagger:hilt-compiler") // False positives
+ }
+ }
+ project(":library:jsonviewer") {
+ onUnusedDependencies {
+ exclude("org.json:json") // Used in unit tests, overwrites the one bundled into Android
+ }
+ }
+ project(":library:ui-styles") {
+ onUnusedDependencies {
+ exclude("com.github.vector-im:PFLockScreen-Android") // False positive
+ }
+ }
+ project(":matrix-sdk-android") {
+ onUnusedDependencies {
+ exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx
+ }
+ }
+ project(":matrix-sdk-android-flow") {
+ onUnusedDependencies {
+ exclude("androidx.paging:paging-runtime-ktx") // False positive
+ }
+ }
+ project(":vector") {
+ onUnusedDependencies {
+ // False positives
+ exclude(
+ "com.vanniktech:emoji-google",
+ "com.vanniktech:emoji-material",
+ "org.maplibre.gl:android-plugin-annotation-v9",
+ "org.maplibre.gl:android-sdk",
+ )
+ }
+ }
+ }
+}
diff --git a/changelog.d/5278.wip b/changelog.d/5278.wip
deleted file mode 100644
index c6014dc9ac..0000000000
--- a/changelog.d/5278.wip
+++ /dev/null
@@ -1 +0,0 @@
-Adds email input and verification screens to the new FTUE onboarding flow
diff --git a/changelog.d/5528.bugfix b/changelog.d/5528.bugfix
new file mode 100644
index 0000000000..b0dc759ab0
--- /dev/null
+++ b/changelog.d/5528.bugfix
@@ -0,0 +1 @@
+Fix cases of missing, swapped, or duplicated messages
diff --git a/changelog.d/5658.feature b/changelog.d/5658.feature
deleted file mode 100644
index ba41a03207..0000000000
--- a/changelog.d/5658.feature
+++ /dev/null
@@ -1 +0,0 @@
-Space explore screen changes: removed space card, added rooms filtering
diff --git a/changelog.d/5689.wip b/changelog.d/5689.wip
deleted file mode 100644
index ccea1ec541..0000000000
--- a/changelog.d/5689.wip
+++ /dev/null
@@ -1 +0,0 @@
-[Live location sharing] Update message in timeline during the live
diff --git a/changelog.d/5724.sdk b/changelog.d/5724.sdk
deleted file mode 100644
index 5a0a37fe31..0000000000
--- a/changelog.d/5724.sdk
+++ /dev/null
@@ -1 +0,0 @@
-- Notifies other devices when a verification request sent from an Android device is accepted.`
diff --git a/changelog.d/5728.misc b/changelog.d/5728.misc
deleted file mode 100644
index 6e463fa76f..0000000000
--- a/changelog.d/5728.misc
+++ /dev/null
@@ -1 +0,0 @@
-leaving space experience changed to be aligned with iOS
diff --git a/changelog.d/5970.feature b/changelog.d/5970.feature
new file mode 100644
index 0000000000..b65decdd2d
--- /dev/null
+++ b/changelog.d/5970.feature
@@ -0,0 +1 @@
+Make read receipt avatar list more compact
diff --git a/changelog.d/6025.misc b/changelog.d/6025.misc
deleted file mode 100644
index 2900796792..0000000000
--- a/changelog.d/6025.misc
+++ /dev/null
@@ -1 +0,0 @@
-@Ignore a number of tests that are currently failing in CI.
diff --git a/changelog.d/6032.bugfix b/changelog.d/6032.bugfix
deleted file mode 100644
index c20d7ddd08..0000000000
--- a/changelog.d/6032.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Revert: Use member name instead of room name in DM creation item
diff --git a/changelog.d/6041.misc b/changelog.d/6041.misc
deleted file mode 100644
index 50378ea3fd..0000000000
--- a/changelog.d/6041.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove ShortcutBadger lib and usage (it was dead code)
diff --git a/changelog.d/6074.bugfix b/changelog.d/6074.bugfix
deleted file mode 100644
index 692dce28d7..0000000000
--- a/changelog.d/6074.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Poll refactoring with unit tests
diff --git a/changelog.d/6089.misc b/changelog.d/6089.misc
deleted file mode 100644
index 19b951c1a3..0000000000
--- a/changelog.d/6089.misc
+++ /dev/null
@@ -1 +0,0 @@
-Test: Ensure calling 'fail()' is not caught by the catch block
diff --git a/changelog.d/6093.sdk b/changelog.d/6093.sdk
new file mode 100644
index 0000000000..8ac5e41b61
--- /dev/null
+++ b/changelog.d/6093.sdk
@@ -0,0 +1 @@
+Allowing AuthenticationService.getLoginFlow to fail without resetting state from previously successful calls
diff --git a/changelog.d/6095.bugfix b/changelog.d/6095.bugfix
deleted file mode 100644
index 11110bfa08..0000000000
--- a/changelog.d/6095.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Correct .well-known/matrix/client handling for server_names which include ports.
diff --git a/changelog.d/6098.feature b/changelog.d/6098.feature
deleted file mode 100644
index 659da42094..0000000000
--- a/changelog.d/6098.feature
+++ /dev/null
@@ -1 +0,0 @@
-Labs flag for enabling live location sharing
diff --git a/changelog.d/6103.bugfix b/changelog.d/6103.bugfix
deleted file mode 100644
index 12e6836460..0000000000
--- a/changelog.d/6103.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Glide - Use current drawable while loading new static map image
diff --git a/changelog.d/6109.bugfix b/changelog.d/6109.bugfix
deleted file mode 100644
index 43b1d610c7..0000000000
--- a/changelog.d/6109.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix sending multiple invites to a room reaching only one or two people
diff --git a/dependencies.gradle b/dependencies.gradle
index 10f9539e5a..dcffb06da6 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -7,10 +7,13 @@ ext.versions = [
'targetCompat' : JavaVersion.VERSION_11,
]
-def gradle = "7.2.0"
+
+// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142
+// Please test carefully before upgrading again.
+def gradle = "7.1.3"
// Ref: https://kotlinlang.org/releases.html
def kotlin = "1.6.21"
-def kotlinCoroutines = "1.6.1"
+def kotlinCoroutines = "1.6.2"
def dagger = "2.42"
def retrofit = "2.9.0"
def arrow = "0.8.2"
@@ -23,7 +26,7 @@ def mavericks = "2.6.1"
def glide = "4.13.2"
def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
-def vanniktechEmoji = "0.9.0"
+def vanniktechEmoji = "0.15.0"
// Testing
def mockk = "1.12.4"
@@ -45,12 +48,13 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
- 'appCompat' : "androidx.appcompat:appcompat:1.4.1",
+ 'activity' : "androidx.activity:activity:1.4.0",
+ 'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'core' : "androidx.core:core-ktx:1.7.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
- 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3",
+ 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
@@ -69,10 +73,12 @@ ext.libs = [
'testRules' : "androidx.test:rules:$androidxTest",
'espressoCore' : "androidx.test.espresso:espresso-core:$espresso",
'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso",
- 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
+ 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso",
+ 'viewpager2' : "androidx.viewpager2:viewpager2:1.0.0",
+ 'transition' : "androidx.transition:transition:1.2.0",
],
google : [
- 'material' : "com.google.android.material:material:1.6.0"
+ 'material' : "com.google.android.material:material:1.6.1"
],
dagger : [
'dagger' : "com.google.dagger:dagger:$dagger",
@@ -81,7 +87,7 @@ ext.libs = [
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
],
squareup : [
- 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi",
+ 'moshi' : "com.squareup.moshi:moshi:$moshi",
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
@@ -107,6 +113,10 @@ ext.libs = [
'mavericks' : "com.airbnb.android:mavericks:$mavericks",
'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks"
],
+ maplibre : [
+ 'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2",
+ 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0"
+ ],
mockk : [
'mockk' : "io.mockk:mockk:$mockk",
'mockkAndroid' : "io.mockk:mockk-android:$mockk"
diff --git a/docs/add_threePids.md b/docs/add_threePids.md
index 6fb0aff426..8d02702ca6 100644
--- a/docs/add_threePids.md
+++ b/docs/add_threePids.md
@@ -1,5 +1,34 @@
# Adding and removing ThreePids to an account
+
+
+* [Add email](#add-email)
+ * [User enter the email](#user-enter-the-email)
+ * [The email is already added to an account](#the-email-is-already-added-to-an-account)
+ * [The email is free](#the-email-is-free)
+* [User receives an e-mail](#user-receives-an-e-mail)
+ * [User clicks on the link](#user-clicks-on-the-link)
+ * [User returns on Element](#user-returns-on-element)
+ * [User enters his password](#user-enters-his-password)
+ * [The link has not been clicked](#the-link-has-not-been-clicked)
+ * [Wrong password](#wrong-password)
+ * [The link has been clicked and the account password is correct](#the-link-has-been-clicked-and-the-account-password-is-correct)
+* [Remove email](#remove-email)
+ * [User want to remove an email from his account](#user-want-to-remove-an-email-from-his-account)
+ * [Email was not bound to an identity server](#email-was-not-bound-to-an-identity-server)
+ * [Email was bound to an identity server](#email-was-bound-to-an-identity-server)
+* [Add phone number](#add-phone-number)
+ * [The phone number is already added to an account](#the-phone-number-is-already-added-to-an-account)
+ * [The phone number is free](#the-phone-number-is-free)
+* [User receive a text message](#user-receive-a-text-message)
+ * [User enter the code to the app](#user-enter-the-code-to-the-app)
+ * [Wrong code](#wrong-code)
+ * [Correct code](#correct-code)
+* [Remove phone number](#remove-phone-number)
+ * [User wants to remove a phone number from his account](#user-wants-to-remove-a-phone-number-from-his-account)
+
+
+
## Add email
### User enter the email
diff --git a/docs/analytics.md b/docs/analytics.md
index 135ace81b0..9b2c176912 100644
--- a/docs/analytics.md
+++ b/docs/analytics.md
@@ -1,5 +1,13 @@
# Analytics in Element
+
+
+* [Solution](#solution)
+* [How to add a new Event](#how-to-add-a-new-event)
+* [Forks of Element](#forks-of-element)
+
+
+
## Solution
Element is using PostHog to send analytics event.
diff --git a/docs/color_migration_guide.md b/docs/color_migration_guide.md
index 31a531d124..f83a88e2b2 100644
--- a/docs/color_migration_guide.md
+++ b/docs/color_migration_guide.md
@@ -1,5 +1,14 @@
# Color migration
+
+
+ * [Changes](#changes)
+ * [Main change for developers](#main-change-for-developers)
+ * [Remaining work](#remaining-work)
+ * [Migration guide](#migration-guide)
+
+
+
### Changes
- use colors defined in https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
diff --git a/docs/design.md b/docs/design.md
index a79f19cf3e..f390725560 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -1,5 +1,31 @@
# Element Android design
+
+
+* [Introduction](#introduction)
+* [How to import from Figma to the Element Android project](#how-to-import-from-figma-to-the-element-android-project)
+ * [Colors](#colors)
+ * [Text](#text)
+ * [Dimension, position and margin](#dimension-position-and-margin)
+ * [Icons](#icons)
+ * [Export drawable from Figma](#export-drawable-from-figma)
+ * [Import in Android Studio](#import-in-android-studio)
+ * [Images](#images)
+* [Figma links](#figma-links)
+ * [Coumpound](#coumpound)
+ * [Login](#login)
+ * [Login v2](#login-v2)
+ * [Room list](#room-list)
+ * [Timeline](#timeline)
+ * [Voice message](#voice-message)
+ * [Room settings](#room-settings)
+ * [VoIP](#voip)
+ * [Presence](#presence)
+ * [Spaces](#spaces)
+ * [List to be continued...](#list-to-be-continued)
+
+
+
## Introduction
Design at element.io is done using Figma - https://www.figma.com
diff --git a/docs/identity_server.md b/docs/identity_server.md
index e765ae3949..a1ee86cb19 100644
--- a/docs/identity_server.md
+++ b/docs/identity_server.md
@@ -1,5 +1,19 @@
# Identity server
+
+
+* [Introduction](#introduction)
+* [Implementation](#implementation)
+* [Related MSCs](#related-mscs)
+* [Steps and requirements](#steps-and-requirements)
+* [Screens](#screens)
+ * [Settings](#settings)
+ * [Discovery screen](#discovery-screen)
+ * [Set identity server screen](#set-identity-server-screen)
+* [Ref:](#ref:)
+
+
+
Issue: #607
PR: #1354
diff --git a/docs/integration_tests.md b/docs/integration_tests.md
index e79f966d1f..b5a830e7ff 100644
--- a/docs/integration_tests.md
+++ b/docs/integration_tests.md
@@ -1,5 +1,18 @@
# Integration tests
+
+
+* [Pre requirements](#pre-requirements)
+* [Install and run Synapse](#install-and-run-synapse)
+* [Run the test](#run-the-test)
+* [Stop Synapse](#stop-synapse)
+* [Troubleshoot](#troubleshoot)
+ * [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver)
+ * [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost:8080")
+ * [virtualenv command fails](#virtualenv-command-fails)
+
+
+
Integration tests are useful to ensure that the code works well for any use cases.
They can also be used as sample on how to use the Matrix SDK.
diff --git a/docs/jitsi.md b/docs/jitsi.md
index 1b4e0a37b4..4dd06effdb 100644
--- a/docs/jitsi.md
+++ b/docs/jitsi.md
@@ -1,20 +1,32 @@
# Jitsi in Element Android
+
+
+* [Native Jitsi SDK](#native-jitsi-sdk)
+ * [How to build the Jitsi Meet SDK](#how-to-build-the-jitsi-meet-sdk)
+ * [Jitsi version](#jitsi-version)
+ * [Run the build script](#run-the-build-script)
+ * [Link with the new generated library](#link-with-the-new-generated-library)
+ * [Sanity tests](#sanity-tests)
+ * [Export the build library](#export-the-build-library)
+
+
+
Native Jitsi support has been added to Element Android by the PR [#1914](https://github.com/vector-im/element-android/pull/1914). The description of the PR contains some documentation about the behaviour in each possible room configuration.
Also, ensure to have a look on [the documentation from Element Web](https://github.com/vector-im/element-web/blob/develop/docs/jitsi.md)
The official documentation about how to integrate the Jitsi SDK in an Android app is available here: https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk.
-# Native Jitsi SDK
+## Native Jitsi SDK
The Jitsi SDK is built by ourselves with the flag LIBRE_BUILD, to be able to be integrated on the F-Droid version of Element Android.
The generated maven repository is then host in the project https://github.com/vector-im/jitsi_libre_maven
-## How to build the Jitsi Meet SDK
+### How to build the Jitsi Meet SDK
-### Jitsi version
+#### Jitsi version
Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`.
@@ -22,7 +34,7 @@ Latest tag can be found from this page: https://github.com/jitsi/jitsi-meet-rele
Currently we are building the version with the tag `android-sdk-3.10.0`.
-### Run the build script
+#### Run the build script
At the root of the Element Android, run the following script:
@@ -32,7 +44,7 @@ At the root of the Element Android, run the following script:
It will build the Jitsi Meet Android library and put every generated files in the folder `/tmp/jitsi`
-### Link with the new generated library
+#### Link with the new generated library
- Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line:
@@ -57,7 +69,7 @@ implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar'
- Perform a gradle sync and build the project
- Perform test
-### Sanity tests
+#### Sanity tests
In order to validate that the upgrade of the Jitsi and WebRTC dependency does not break anything, the following sanity tests have to be performed, using two devices:
- Make 1-1 audio call (so using WebRTC)
@@ -65,7 +77,7 @@ In order to validate that the upgrade of the Jitsi and WebRTC dependency does no
- Create and join a conference call with audio only (so using Jitsi library). Leave the conference. Join it again.
- Create and join a conference call with audio and video (so using Jitsi library) Leave the conference. Join it again.
-### Export the build library
+#### Export the build library
If all the tests are passed, you can export the generated Jitsi library to our Maven repository.
@@ -81,4 +93,4 @@ url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.
- Build the project and perform the sanity tests again.
-- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android.
\ No newline at end of file
+- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android.
diff --git a/docs/notifications.md b/docs/notifications.md
index b44984785a..612b8785b8 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -1,37 +1,42 @@
This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app.
# Table of Contents
-1. [Prerequisites Knowledge](#prerequisites-knowledge)
- * [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver)
- * [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification)
- * [Push VS Notification](#push-vs-notification)
- * [Push in the matrix federated world](#push-in-the-matrix-federated-world)
- * [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client)
- * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
- * [Background processing limitations](#background-processing-limitations)
-2. [Element Notification implementations](#element-notification-implementations)
- * [Requirements](#requirements)
- * [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid)
- * [Push (FCM) received in background](#push-fcm-received-in-background)
- * [FCM Fallback mode](#fcm-fallback-mode)
- * [F-Droid background Mode](#f-droid-background-mode)
-3. [Application Settings](#application-settings)
+
+
+
+* [Prerequisites Knowledge](#prerequisites-knowledge)
+ * [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?)
+ * [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification)
+ * [Push VS Notification](#push-vs-notification)
+ * [Push in the matrix federated world](#push-in-the-matrix-federated-world)
+ * [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?)
+ * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
+ * [Background processing limitations](#background-processing-limitations)
+* [Element Notification implementations](#element-notification-implementations)
+ * [Requirements](#requirements)
+ * [Foreground sync mode (Gplay and F-Droid)](#foreground-sync-mode-gplay-and-f-droid)
+ * [Push (FCM) received in background](#push-fcm-received-in-background)
+ * [FCM Fallback mode](#fcm-fallback-mode)
+ * [F-Droid background Mode](#f-droid-background-mode)
+* [Application Settings](#application-settings)
+
+
First let's start with some prerequisite knowledge
-# Prerequisites Knowledge
+## Prerequisites Knowledge
-## How does a matrix client get a message from a homeserver?
+### How does a matrix client get a message from a homeserver?
In order to get messages from a homeserver, a matrix client need to perform a ``sync`` operation.
`To read events, the intended flow of operation is for clients to first call the /sync API without a since parameter. This returns the most recent message events for each room, as well as the state of the room at the start of the returned timeline. `
-The client need to call the `sync`API periodically in order to get incremental updates of the server state (new messages).
+The client need to call the `sync` API periodically in order to get incremental updates of the server state (new messages).
This mechanism is known as **HTTP long Polling**.
-Using the **HTTP Long Polling** mechanism a client polls a server requesting new information.
+Using the **HTTP Long Polling** mechanism a client polls a server requesting new information.
The server *holds the request open until new data is available*.
Once available, the server responds and sends the new information.
When the client receives the new information, it immediately sends another request, and the operation is repeated.
@@ -52,7 +57,7 @@ By default, this is 0, so the server will return immediately even if the respons
When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
-## How does a mobile app receives push notification
+### How does a mobile app receives push notification
Push notification is used as a way to wake up a mobile application when some important information is available and should be processed.
@@ -66,22 +71,22 @@ FCM will only work on android devices that have Google plays services installed
(In simple terms, Google Play Services is a background service that runs on Android, which in turn helps in integrating Google’s advanced functionalities to other applications)
De-Googlified devices need to rely on something else in order to stay up to date with a server.
-There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls- ,
- privacy and or independency requirement, source code licence)
+There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls-,
+ privacy and or independence requirement, source code licence)
-## Push VS Notification
+### Push VS Notification
This need some disambiguation, because it is the source of common confusion:
-*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH plateform.*
+*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH platform.*
Technically there is a difference between a push and a notification. A notification is what you see on screen and/or in the notification Menu/Drawer (in the top bar of the phone).
Notifications are not always triggered by a push (One can display a notification locally triggered by an alarm)
-## Push in the matrix federated world
+### Push in the matrix federated world
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
This server is called a **Push Gateway** in the matrix world
@@ -118,11 +123,11 @@ Client/Server API + | | | | |
```
Recommended reading:
- * https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html
+* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id128
-## How does the homeserver know when to notify a client?
+### How does the homeserver know when to notify a client?
This is defined by [**push rules**](https://matrix.org/docs/spec/client_server/r0.4.0.html#push-rules-).
@@ -140,14 +145,14 @@ Of course, content patterns matching cannot be used for encrypted messages serve
That is why clients are able to **process the push rules client side** to decide what kind of notification should be presented for a given event.
-## Push vs privacy, and mitigation
+### Push vs privacy, and mitigation
As seen previously, App developers don't directly send a push to the end user's device, they use a Push Provider as intermediary. So technically this intermediary is able to read the content of what is sent.
App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to it's server in order to generate a local notification.
-## Background processing limitations
+### Background processing limitations
A mobile applications process live in a managed word, meaning that its process can be limited (e.g no network access), stopped or killed at almost anytime by the Operating System.
@@ -167,15 +172,15 @@ The documentation on this subject is vague, and as per our experiments not alway
It is getting more and more complex to have reliable notifications when FCM is not used.
-# Element Notification implementations
+## Element Notification implementations
-## Requirements
+### Requirements
Element Android must work with and without FCM.
* The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present)
* The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
-## Foreground sync mode (Gplay & F-Droid)
+### Foreground sync mode (Gplay and F-Droid)
When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
@@ -183,9 +188,9 @@ As this mode does not need to live beyond the scope of the application, and as p
This mode is turned on when the app enters foreground, and off when enters background.
-In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
+In background, and depending on whether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
-## Push (FCM) received in background
+### Push (FCM) received in background
In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the homeserver.
@@ -225,10 +230,10 @@ Upon reception of the FCM push, Element will perform a sync call to the homeserv
Element implements several strategies in these cases (TODO document)
-## FCM Fallback mode
+### FCM Fallback mode
It is possible that Element is not able to get a FCM push token.
-Common errors (amoung several others) that can cause that:
+Common errors (among several others) that can cause that:
* Google Play Services is outdated
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
@@ -246,7 +251,7 @@ Usually in this mode, what happen is when you take back your phone in your hand,
The fallback mode is supposed to be a temporary state waiting for the user to fix issues for FCM, or for App Developers that has done a fork to correctly configure their FCM settings.
-## F-Droid background Mode
+### F-Droid background Mode
The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push.
@@ -256,7 +261,7 @@ Only solution left is to use `AlarmManager`, that offers new API to allow launch
Notice that these alarms, due to their potential impact on battery life, can still be restricted by the system. Documentation says that they will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn.
-These restrictions can be relaxed by requirering the app to be white listed from battery optimization.
+These restrictions can be relaxed by requiring the app to be white listed from battery optimization.
F-Droid version will schedule alarms that will then trigger a Broadcast Receiver, that in turn will launch a Service (in the classic android way), and the reschedule an alarm for next time.
@@ -266,9 +271,7 @@ That is why on Element F-Droid, the broadcast receiver will acquire a temporary
Note that foreground services require to put a notification informing the user that the app is doing something even if not launched).
-
-
-# Application Settings
+## Application Settings
**Notifications > Enable notifications for this account**
diff --git a/docs/pull_request.md b/docs/pull_request.md
index 94f8f3b6d8..d2d2bb7a3b 100644
--- a/docs/pull_request.md
+++ b/docs/pull_request.md
@@ -1,5 +1,43 @@
# Pull requests
+
+
+* [Introduction](#introduction)
+* [Who should read this document?](#who-should-read-this-document?)
+* [Submitting PR](#submitting-pr)
+ * [Who can submit pull requests?](#who-can-submit-pull-requests?)
+ * [Humans](#humans)
+ * [Draft PR?](#draft-pr?)
+ * [Base branch](#base-branch)
+ * [PR Review Assignment](#pr-review-assignment)
+ * [PR review time](#pr-review-time)
+ * [Re-request PR review](#re-request-pr-review)
+ * [When create split PR?](#when-create-split-pr?)
+ * [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr)
+ * [Bots](#bots)
+ * [Dependabot](#dependabot)
+ * [Gradle wrapper](#gradle-wrapper)
+ * [Sync analytics plan](#sync-analytics-plan)
+* [Reviewing PR](#reviewing-pr)
+ * [Who can review pull requests?](#who-can-review-pull-requests?)
+ * [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr)
+ * [Rules](#rules)
+ * [Check the form](#check-the-form)
+ * [PR title](#pr-title)
+ * [PR description](#pr-description)
+ * [File change](#file-change)
+ * [Check the commit](#check-the-commit)
+ * [Check the substance](#check-the-substance)
+ * [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr)
+ * [What happen to the issue(s)?](#what-happen-to-the-issues?)
+ * [Merge conflict](#merge-conflict)
+ * [When and who can merge PR](#when-and-who-can-merge-pr)
+ * [Merge type](#merge-type)
+ * [Resolve conversation](#resolve-conversation)
+* [Responsibility](#responsibility)
+
+
+
## Introduction
This document gives some clue about how to efficiently manage Pull Requests (PR). This document is a first draft and may be improved later.
diff --git a/docs/signin.md b/docs/signin.md
index 0a234d2a20..65e305389f 100644
--- a/docs/signin.md
+++ b/docs/signin.md
@@ -2,6 +2,27 @@
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
+
+
+* [Sign in flows](#sign-in-flows)
+ * [Get the flow](#get-the-flow)
+ * [Login with username](#login-with-username)
+ * [Incorrect password](#incorrect-password)
+ * [Correct password:](#correct-password:)
+ * [Login with email](#login-with-email)
+ * [Unknown email](#unknown-email)
+ * [Known email, wrong password](#known-email-wrong-password)
+ * [Known email, correct password](#known-email-correct-password)
+ * [Login with Msisdn](#login-with-msisdn)
+ * [Login with SSO](#login-with-sso)
+* [Reset password](#reset-password)
+ * [Send email](#send-email)
+ * [When the email is not known](#when-the-email-is-not-known)
+ * [When the email is known](#when-the-email-is-known)
+ * [User clicks on the link](#user-clicks-on-the-link)
+
+
+
## Sign in flows
### Get the flow
@@ -322,4 +343,4 @@ curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":
{}
```
-The password has been changed, and all the existing token are invalidated. User can now login with the new password.
\ No newline at end of file
+The password has been changed, and all the existing token are invalidated. User can now login with the new password.
diff --git a/docs/signup.md b/docs/signup.md
index 97cd20a423..0b01338965 100644
--- a/docs/signup.md
+++ b/docs/signup.md
@@ -4,6 +4,20 @@ This document describes the flow of registration to a homeserver. Examples come
*Ref*: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
+
+
+* [Sign up flows](#sign-up-flows)
+ * [First step](#first-step)
+ * [Step 1: entering user name and password](#step-1:-entering-user-name-and-password)
+ * [If username already exists](#if-username-already-exists)
+ * [Step 2: entering email](#step-2:-entering-email)
+ * [Step 2 bis: user enters an email](#step-2-bis:-user-enters-an-email)
+ * [Step 3: Accepting T&C](#step-3:-accepting-t&c)
+ * [Step 4: Captcha](#step-4:-captcha)
+ * [Step 5: MSISDN](#step-5:-msisdn)
+
+
+
## Sign up flows
### First step
diff --git a/docs/ui-tests.md b/docs/ui-tests.md
index 667a6ed7fb..d0b9db6818 100644
--- a/docs/ui-tests.md
+++ b/docs/ui-tests.md
@@ -10,6 +10,20 @@ Currently the test are covering a small set of application flows:
- Self verification via emoji
- Self verification via passphrase
+
+
+* [Prerequisites:](#prerequisites:)
+* [Run the tests](#run-the-tests)
+ * [From the source code](#from-the-source-code)
+ * [From command line](#from-command-line)
+* [Recipes](#recipes)
+ * [Wait for initial sync](#wait-for-initial-sync)
+ * [Accessing current activity](#accessing-current-activity)
+ * [Interact with other session](#interact-with-other-session)
+ * [Contributing to the UiAllScreensSanityTest](#contributing-to-the-uiallscreenssanitytest)
+
+
+
## Prerequisites:
Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses).
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt
new file mode 100644
index 0000000000..d369efa9f2
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Zlepšení správy ignorovaných uživatelů. Opravy různých chyb a vylepšení stability.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/en-US/changelogs/40104180.txt b/fastlane/metadata/android/en-US/changelogs/40104180.txt
new file mode 100644
index 0000000000..61db61727a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104180.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Various bug fixes and stability improvements.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/es-ES/changelogs/40104140.txt b/fastlane/metadata/android/es-ES/changelogs/40104140.txt
new file mode 100644
index 0000000000..595cd6b3d4
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Cambios principales en esta versión: Mejoras en la administración de usuarios ignorados. Varias correciones de bugs y mejoras en la estabilidad.
+Registro de cambios: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40104140.txt b/fastlane/metadata/android/et/changelogs/40104140.txt
new file mode 100644
index 0000000000..9000616f38
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: eiratud kasutajate parem haldus ning erinevate vigade parandused ja stabiilsust edendavad kohendused.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40104140.txt b/fastlane/metadata/android/fa/changelogs/40104140.txt
new file mode 100644
index 0000000000..cf5d7bc6ac
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: مدیریت بهبودیافتهٔ کاربران چشمپوشیده. رفع اشکالهای مختلف و بهبودهای پایداری.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104100.txt b/fastlane/metadata/android/fr-FR/changelogs/40104100.txt
new file mode 100644
index 0000000000..f2453c5539
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104100.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Défilement dans les messages vocaux. Plusieurs corrections de bogues et d’améliorations de stabilité.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104110.txt b/fastlane/metadata/android/fr-FR/changelogs/40104110.txt
new file mode 100644
index 0000000000..fe61fd021c
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104110.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104120.txt b/fastlane/metadata/android/fr-FR/changelogs/40104120.txt
new file mode 100644
index 0000000000..accd82fe72
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104120.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Les utilisateurs peuvent apparaître hors-ligne. Ajout d’un lecteur pour les pièces jointes audio
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104130.txt b/fastlane/metadata/android/fr-FR/changelogs/40104130.txt
new file mode 100644
index 0000000000..accd82fe72
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104130.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Les utilisateurs peuvent apparaître hors-ligne. Ajout d’un lecteur pour les pièces jointes audio
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104140.txt b/fastlane/metadata/android/fr-FR/changelogs/40104140.txt
new file mode 100644
index 0000000000..087d5bc1c8
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Amélioration de la gestion des utilisateurs ignorés. Plusieurs corrections de bogues et d’améliorations de stabilité.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40104140.txt b/fastlane/metadata/android/id/changelogs/40104140.txt
new file mode 100644
index 0000000000..174e8dcded
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Tingkatkan pengelolaan pengguna yang diabaikan. Beberapa perbaikan kutu dan stabilitas.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40104140.txt b/fastlane/metadata/android/it-IT/changelogs/40104140.txt
new file mode 100644
index 0000000000..ae367e72d0
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: migliorata la gestione degli utenti ignorati. Varie correzioni e miglioramenti della stabilità.
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/lo-LA/changelogs/40104140.txt b/fastlane/metadata/android/lo-LA/changelogs/40104140.txt
new file mode 100644
index 0000000000..e8c8d84031
--- /dev/null
+++ b/fastlane/metadata/android/lo-LA/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງການບໍລິຫານການລະເວັ້ນຜູ້ໃຊ້. ປັບປຸງບັກ ແລະຄວາມສະຖຽນ.
+ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101120.txt b/fastlane/metadata/android/pl-PL/changelogs/40101120.txt
new file mode 100644
index 0000000000..6a62f1c6a9
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40101120.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: aktualizacja motywu i stylu oraz naprawa awarii po rozmowie wideo
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.12
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101130.txt b/fastlane/metadata/android/pl-PL/changelogs/40101130.txt
new file mode 100644
index 0000000000..ec8d488eb8
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40101130.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: głównie aktualizacja stabilności i poprawki błędów.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.13
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101140.txt b/fastlane/metadata/android/pl-PL/changelogs/40101140.txt
new file mode 100644
index 0000000000..c4c102da4a
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40101140.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: naprawienie problemu z zaszyfrowanymi wiadomościami.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.14
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101150.txt b/fastlane/metadata/android/pl-PL/changelogs/40101150.txt
new file mode 100644
index 0000000000..2eb1a3f018
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40101150.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: implementacja wiadomości głosowych w ustawieniach laboratorium.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.15
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101160.txt b/fastlane/metadata/android/pl-PL/changelogs/40101160.txt
new file mode 100644
index 0000000000..682da8be76
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40101160.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Naprawiono błąd podczas wysyłania zaszyfrowanej wiadomości, jeśli ktoś w pokoju się wyloguje.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.16
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40102000.txt b/fastlane/metadata/android/pl-PL/changelogs/40102000.txt
new file mode 100644
index 0000000000..cdae0a4ba7
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40102000.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Wiadomość głosowa jest domyślnie włączona.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.2.0
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40102010.txt b/fastlane/metadata/android/pl-PL/changelogs/40102010.txt
new file mode 100644
index 0000000000..0a825e8672
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40102010.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Wiele ulepszeń w VoIP i Przestrzeniach (nadal w wersji beta).
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.2.1
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103000.txt b/fastlane/metadata/android/pl-PL/changelogs/40103000.txt
new file mode 100644
index 0000000000..8b408ced72
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40103000.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Organizuj swoje pokoje za pomocą Przestrzeni!
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.0
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103010.txt b/fastlane/metadata/android/pl-PL/changelogs/40103010.txt
new file mode 100644
index 0000000000..0a49e7fa68
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40103010.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Organizuj swoje pokoje za pomocą Przestrzeni! Wersja 1.3.1 naprawia awarię, która może wystąpić w wersji 1.3.0.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.1
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103020.txt b/fastlane/metadata/android/pl-PL/changelogs/40103020.txt
new file mode 100644
index 0000000000..3e37f64b76
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40103020.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Dodano obsługę Android Auto. Wiele poprawek błędów!
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.2
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103030.txt b/fastlane/metadata/android/pl-PL/changelogs/40103030.txt
new file mode 100644
index 0000000000..8f80d95b5a
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40103030.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Uwidocznij politykę(-i) serwera tożsamości w ustawieniach. Tymczasowo usunięto obsługę Androida Auto.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.3
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103040.txt b/fastlane/metadata/android/pl-PL/changelogs/40103040.txt
new file mode 100644
index 0000000000..13458a7b2d
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40103040.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Dodanie obsługi obecności, dla pokoju wiadomości bezpośrednich (uwaga: obecność jest wyłączona na matrix.org). Dodano ponownie obsługę Androida Auto.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.4
diff --git a/fastlane/metadata/android/pl-PL/changelogs/40104140.txt b/fastlane/metadata/android/pl-PL/changelogs/40104140.txt
new file mode 100644
index 0000000000..84ab57a2ac
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Główne zmiany w tej wersji: Poprawa zarządzania ignorowanymi użytkownikami. Różne poprawki błędów i ulepszenia stabilności.
+Pełna lista zmian: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104140.txt b/fastlane/metadata/android/pt-BR/changelogs/40104140.txt
new file mode 100644
index 0000000000..ed1a53c910
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: Melhorar gerenciamento de usuárias(os) ignoradas(os). Vários consertos de bugs e melhorias de estabilidade.
+Changelog completo: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40104140.txt b/fastlane/metadata/android/sk/changelogs/40104140.txt
new file mode 100644
index 0000000000..e1c85961a4
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Zlepšenie správy ignorovaných používateľov. Rôzne opravy chýb a vylepšenia stability.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104140.txt b/fastlane/metadata/android/sv-SE/changelogs/40104140.txt
new file mode 100644
index 0000000000..9b58878dfb
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: Förbättra hantering av ignorerade användare. Diverse buggfixar och stabilitetsförbättringar.
+Full ändringslogg: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ta-IN/changelogs/40104140.txt b/fastlane/metadata/android/ta-IN/changelogs/40104140.txt
new file mode 100644
index 0000000000..f3196b57b6
--- /dev/null
+++ b/fastlane/metadata/android/ta-IN/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+இந்த பதிப்பில் உள்ள முதன்மை மாற்றங்கள்: தவிர்க்கப்பட்ட பயனர்களின் மேலாண்மை மேம்படுத்தப்பட்டுள்ளது. வெவ்வேறு வழுக்களைச் சரிசெய்தல் மற்றும் நிலைப்புத்தன்மையை மேம்படுத்தல்.
+முழு மாற்ற அறிக்கை: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/ta-IN/full_description.txt b/fastlane/metadata/android/ta-IN/full_description.txt
new file mode 100644
index 0000000000..9aa693bda2
--- /dev/null
+++ b/fastlane/metadata/android/ta-IN/full_description.txt
@@ -0,0 +1,42 @@
+Element is both a secure messenger and a productivity team collaboration app that is ideal for group chats while remote working. This chat app uses end-to-end encryption to provide powerful video conferencing, file sharing and voice calls.
+
+Element இன் தனிச்சிறப்புகளுள் சில:
+- மேம்பட்ட இயங்கலை தொடர்பு கருவிகள்
+- தொலைநிலையில் உள்ள ஊழியர்களுக்கும், பாதுகாப்பான நிறும கருத்து பரிமாற்றங்களை அனுமதிப்பதற்காக, முழுவதுமாக மறைகுறியாக்கப்பட்ட செய்திகள்
+- MATRIX திறந்த மூல கட்டமைப்பை அடிப்படையாக கொண்டு செயல்படும் அதிகாரப்பரவலாக்கப்பட்ட அரட்டை
+- செயல்திட்டங்களை மேலாண்மை செய்யும் போது, மறைகுறியாக்கப்பட்ட தரவுடன் கூடிய பாதுகாப்பான கோப்பு பகிரல்
+- IP மூலம் குரல் (VoIP) மற்றும் திரை பகிரல் உடன் கூடிய குரல் அரட்டைகள்
+- உங்கள் மனம் கவர்ந்த இயங்கலை உடனிணைவு கருவிகள், செயல்திட்ட மேலாண்மை கருவிகள், VoIP சேவைகள் மற்றும் இதர குழு தூதுரை செயலிகள் உடன் கூடிய எளிமையான ஒருமைப்பாடு
+
+Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages.
+
+தனியுரிமை மற்றும் மறைகுறியாக்கப்பட்ட செய்தி அனுப்பல்
+தேவையில்லாத விளம்பரங்கள், தரவு சுரண்டல் மற்றும் தகவல் கட்டுப்பாடு போன்றவற்றில் இருந்து Element உங்களை பாதுகாக்கிறது. மேலும், இது முனைக்கு-முனை மறைகுறியாக்கம் மற்றும் குறுக்கு-ஒப்பமிடப்பட்ட சாதன சரிபார்ப்பு ஆகியவற்றின் மூலம் உங்கள் எல்லா தரவுகள், ஒன்றுக்கொன்றான காணொளி மற்றும் குரல் அழைப்புகளை பாதுகாக்கிறது.
+
+Element gives you control over your privacy while allowing you to communicate securely with யாரோனும் ஒருவருடன் on the Matrix network, or other business collaboration tools by integrating with apps such as Slack.
+
+Element can be self-hosted
+To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility.
+
+உங்கள் தரவைச் சொந்தமாக்கிக் கொள்ளுங்கள்
+தரவுகள் மற்றும் செய்திகளை எங்கு சேமித்து வைக்க வேண்டும் என்பதை நீங்கள் முடிவு செய்கிறீர்கள். இதன்மூலம், தரவு சுரண்டல் மற்றும் மூன்றாம் தரப்பினர் அனுகல் ஆகிய இடர்களை தவிர்க்கலாம்.
+
+Element வெவ்வேறு வகையில் கட்டுப்பாட்டை உங்களிடம் அளிக்கிறது:
+1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers
+2. Self-host your account by running a server on your own IT infrastructure
+3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform
+
+திறந்த செய்தி அனுப்பல் மற்றும் ஒருமைப்பாடு
+You can chat with anyone on the Matrix network, whether they’re using Element, another Matrix app or even if they are using a different messaging app.
+
+மிகவும் பாதுகாப்பானது
+உண்மையான முனைக்கு-முனை மறைகுறியாக்கம் (உரையாடலில் உள்ளவர்கள் மட்டுமே மறைகுறியாக்கத்தை நீக்கி செய்தியை காண இயலும்) மற்றும் குறுக்கு-ஒப்பமிடப்பட்ட சாதன சரிபார்ப்பு.
+
+முழுமையான தொடர்பு மற்றும் ஒருமைப்பாடு
+செய்தி அனுப்பல், காணொளி மற்றும் குரல் அழைப்புகளை, கோப்பு பகிரல், திரை பகிரல் மற்றும் ஒருமைப்பாடுகள், இயலிகள் மற்றும் நிரல் பலகைகளின் மொத்த கொத்து. அறைகள், குழுக்களை உருவாக்கி, அவர்களுடன் உரையாடி, வேலையை எளிமையாக்கவும்.
+
+எங்கு விட்டு சென்றீர்களோ அதிலிருந்த துவங்கவும்
+Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io
+
+திறந்த மூலம்
+Element Android ஒரு திறந்த மூல செயல் திட்டமாகும். இது GitHub இல் தொகுத்து வழங்கப்பட்டுள்ளது. வழுக்கள் ஏதேனும் கண்டறிந்தால் மற்றும்/அல்லது இதன் வளர்ச்சிக்கு பங்களிக்க விரும்பினால், https://github.com/vector-im/element-android என்னும் தளத்திற்கு வருகை தரவும்.
diff --git a/fastlane/metadata/android/ta-IN/short_description.txt b/fastlane/metadata/android/ta-IN/short_description.txt
new file mode 100644
index 0000000000..9c7afb2a37
--- /dev/null
+++ b/fastlane/metadata/android/ta-IN/short_description.txt
@@ -0,0 +1 @@
+மறைகுறியாக்கப்பட்ட செய்தி அனுப்பல், குழு அரட்டை மற்றும் காணொளி அழைப்புகள்
diff --git a/fastlane/metadata/android/ta-IN/title.txt b/fastlane/metadata/android/ta-IN/title.txt
new file mode 100644
index 0000000000..ecb9a01c06
--- /dev/null
+++ b/fastlane/metadata/android/ta-IN/title.txt
@@ -0,0 +1 @@
+Element - பாதுகாப்பான தூதுரை சேவை
diff --git a/fastlane/metadata/android/uk/changelogs/40104140.txt b/fastlane/metadata/android/uk/changelogs/40104140.txt
new file mode 100644
index 0000000000..293ad117e4
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+Основні зміни у цій версії: Удосконалено керування нехтуваними користувачами. Різні виправлення помилок та поліпшення стабільності.
+Вичерпний журнал змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104140.txt b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt
new file mode 100644
index 0000000000..ff830dab7c
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:改善被忽略使用者的管理。多個臭蟲修復與穩定性改善。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/library/attachment-viewer/build.gradle b/library/attachment-viewer/build.gradle
index 048710f62c..8bbafd3387 100644
--- a/library/attachment-viewer/build.gradle
+++ b/library/attachment-viewer/build.gradle
@@ -55,5 +55,6 @@ dependencies {
implementation libs.androidx.appCompat
implementation libs.androidx.recyclerview
- implementation libs.google.material
-}
\ No newline at end of file
+ api libs.androidx.viewpager2
+ implementation libs.androidx.transition
+}
diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle
index d3afd8d29b..0f7789a2a8 100644
--- a/library/core-utils/build.gradle
+++ b/library/core-utils/build.gradle
@@ -50,6 +50,5 @@ android {
}
dependencies {
- implementation libs.androidx.appCompat
implementation libs.jetbrains.coroutinesAndroid
}
diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle
index 2110747feb..e1a3b0c9ee 100644
--- a/library/jsonviewer/build.gradle
+++ b/library/jsonviewer/build.gradle
@@ -52,6 +52,7 @@ dependencies {
implementation libs.androidx.appCompat
implementation libs.androidx.core
+ implementation libs.androidx.recyclerview
implementation libs.airbnb.epoxy
kapt libs.airbnb.epoxyProcessor
@@ -60,7 +61,6 @@ dependencies {
// Span utils
implementation 'me.gujun.android:span:1.7'
- implementation libs.google.material
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
diff --git a/library/multipicker/build.gradle b/library/multipicker/build.gradle
index bb98a2f852..2de99d5c20 100644
--- a/library/multipicker/build.gradle
+++ b/library/multipicker/build.gradle
@@ -38,9 +38,9 @@ android {
}
dependencies {
- implementation libs.androidx.appCompat
- implementation libs.androidx.fragmentKtx
+ api libs.androidx.activity
implementation libs.androidx.exifinterface
+ implementation libs.androidx.core
// Log
implementation libs.jakewharton.timber
diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle
index 0ac513b252..31cfdd24c7 100644
--- a/library/ui-styles/build.gradle
+++ b/library/ui-styles/build.gradle
@@ -60,4 +60,4 @@ dependencies {
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
// dialpad dimen
implementation 'im.dlg:android-dialer:1.2.5'
-}
\ No newline at end of file
+}
diff --git a/library/ui-styles/src/main/res/drawable/bg_pin_key.xml b/library/ui-styles/src/main/res/drawable/bg_pin_key.xml
index d4a54577be..5bf293aab0 100644
--- a/library/ui-styles/src/main/res/drawable/bg_pin_key.xml
+++ b/library/ui-styles/src/main/res/drawable/bg_pin_key.xml
@@ -10,4 +10,4 @@
android:height="70dp" />
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml b/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml
index 1827a7682b..879cac15ca 100644
--- a/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml
+++ b/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml
@@ -10,4 +10,4 @@
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml b/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml
index 799ea30174..83bdac5126 100644
--- a/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml
+++ b/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml
@@ -9,4 +9,4 @@
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dots.xml b/library/ui-styles/src/main/res/drawable/pin_code_dots.xml
index 29e445e511..c4b1073f85 100644
--- a/library/ui-styles/src/main/res/drawable/pin_code_dots.xml
+++ b/library/ui-styles/src/main/res/drawable/pin_code_dots.xml
@@ -6,4 +6,4 @@
android:drawable="@drawable/pin_code_dot_fill"/>
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/styles_location.xml b/library/ui-styles/src/main/res/values/styles_location.xml
index 7571265241..9d9fc862f6 100644
--- a/library/ui-styles/src/main/res/values/styles_location.xml
+++ b/library/ui-styles/src/main/res/values/styles_location.xml
@@ -18,4 +18,26 @@
center
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_pin_code.xml b/library/ui-styles/src/main/res/values/styles_pin_code.xml
index 2b6c113359..cb22863694 100644
--- a/library/ui-styles/src/main/res/values/styles_pin_code.xml
+++ b/library/ui-styles/src/main/res/values/styles_pin_code.xml
@@ -41,4 +41,4 @@
?vctr_content_primary
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml
index c86eeb8efb..20c375c2d6 100644
--- a/library/ui-styles/src/main/res/values/styles_timeline.xml
+++ b/library/ui-styles/src/main/res/values/styles_timeline.xml
@@ -1,5 +1,5 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/matrix-sdk-android-flow/build.gradle b/matrix-sdk-android-flow/build.gradle
index ea43ce20c8..fb69af2d82 100644
--- a/matrix-sdk-android-flow/build.gradle
+++ b/matrix-sdk-android-flow/build.gradle
@@ -31,9 +31,7 @@ android {
}
dependencies {
-
implementation project(":matrix-sdk-android")
- implementation libs.androidx.appCompat
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
@@ -41,7 +39,4 @@ dependencies {
// Paging
implementation libs.androidx.pagingRuntimeKtx
-
- // Logging
- implementation libs.jakewharton.timber
}
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
index 9f260858f6..2839e6ab61 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
@@ -45,6 +45,13 @@ import org.matrix.android.sdk.api.util.toOptional
class FlowSession(private val session: Session) {
+ fun liveRoomSummary(roomId: String): Flow> {
+ return session.roomService().getRoomSummaryLive(roomId).asFlow()
+ .startWith(session.coroutineDispatchers.io) {
+ session.roomService().getRoomSummary(roomId).toOptional()
+ }
+ }
+
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow> {
return session.roomService().getRoomSummariesLive(queryParams, sortOrder).asFlow()
.startWith(session.coroutineDispatchers.io) {
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 33798d1357..3829063836 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -56,7 +56,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- buildConfigField "String", "SDK_VERSION", "\"1.4.18\""
+ buildConfigField "String", "SDK_VERSION", "\"1.4.20\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@@ -136,7 +136,6 @@ dependencies {
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
- implementation libs.androidx.appCompat
implementation libs.androidx.core
// Lifecycle
@@ -155,12 +154,11 @@ dependencies {
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor'
- implementation 'com.squareup.okhttp3:okhttp-urlconnection'
implementation libs.squareup.moshi
kapt libs.squareup.moshiKotlin
- implementation libs.markwon.core
+ api "com.atlassian.commonmark:commonmark:0.13.0"
// Image
implementation libs.androidx.exifinterface
@@ -176,10 +174,6 @@ dependencies {
// Work
implementation libs.androidx.work
- // FP
- implementation libs.arrow.core
- implementation libs.arrow.instances
-
// olm lib is now hosted in MavenCentral
implementation 'org.matrix.android:olm-sdk:3.2.11'
@@ -198,11 +192,9 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
testImplementation libs.tests.junit
- testImplementation 'org.robolectric:robolectric:4.7.3'
- //testImplementation 'org.robolectric:shadows-support-v4:3.0'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation libs.mockk.mockk
testImplementation libs.tests.kluent
diff --git a/matrix-sdk-android/docs/packages.md b/matrix-sdk-android/docs/packages.md
index ae7bee1b4e..19f7c15a7a 100644
--- a/matrix-sdk-android/docs/packages.md
+++ b/matrix-sdk-android/docs/packages.md
@@ -1,3 +1,7 @@
+# Package org.matrix.android.sdk.userstories
+
+This package contains some user stories (**Us** prefix) of the SDK usage. You will find example of what it is possible to do with the SDK and the API which can be used to do it.
+
# Package org.matrix.android.sdk.api
This is the root package of the API exposed by this SDK.
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt
index 486bc02769..7e4fc4768f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt
@@ -24,8 +24,8 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@@ -34,32 +34,22 @@ import org.matrix.android.sdk.common.TestConstants
@LargeTest
class AccountCreationTest : InstrumentedTest {
- private val commonTestHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
@Test
- fun createAccountTest() {
- val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
-
- commonTestHelper.signOutAndClose(session)
+ fun createAccountTest() = runSessionTest(context()) { commonTestHelper ->
+ commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
}
@Test
@Ignore("This test will be ignored until it is fixed")
- fun createAccountAndLoginAgainTest() {
+ fun createAccountAndLoginAgainTest() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Log again to the same account
- val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
-
- commonTestHelper.signOutAndClose(session)
- commonTestHelper.signOutAndClose(session2)
+ commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
}
@Test
- fun simpleE2eTest() {
- val res = cryptoTestHelper.doE2ETestWithAliceInARoom()
-
- res.cleanUp(commonTestHelper)
+ fun simpleE2eTest() = runCryptoTest(context()) { cryptoTestHelper, _ ->
+ cryptoTestHelper.doE2ETestWithAliceInARoom()
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
index 6d740c5a34..260e8dbe05 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
@@ -25,7 +25,7 @@ import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.failure.isInvalidPassword
-import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@@ -34,14 +34,12 @@ import org.matrix.android.sdk.common.TestConstants
@Ignore("This test will be ignored until it is fixed")
class ChangePasswordTest : InstrumentedTest {
- private val commonTestHelper = CommonTestHelper(context())
-
companion object {
private const val NEW_PASSWORD = "this is a new password"
}
@Test
- fun changePasswordTest() {
+ fun changePasswordTest() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Change password
@@ -54,9 +52,6 @@ class ChangePasswordTest : InstrumentedTest {
throwable.isInvalidPassword().shouldBeTrue()
// Try to login with the new password, should work
- val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
-
- commonTestHelper.signOutAndClose(session)
- commonTestHelper.signOutAndClose(session2)
+ commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
index d2dfe4d945..0b21f85742 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
@@ -29,7 +29,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
-import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import kotlin.coroutines.Continuation
@@ -39,10 +39,8 @@ import kotlin.coroutines.resume
@FixMethodOrder(MethodSorters.JVM)
class DeactivateAccountTest : InstrumentedTest {
- private val commonTestHelper = CommonTestHelper(context())
-
@Test
- fun deactivateAccountTest() {
+ fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Deactivate the account
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt
index 9371154aaf..8dbff82015 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt
@@ -23,7 +23,7 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
-import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import timber.log.Timber
@@ -32,10 +32,8 @@ import timber.log.Timber
@FixMethodOrder(MethodSorters.JVM)
class ApiInterceptorTest : InstrumentedTest {
- private val commonTestHelper = CommonTestHelper(context())
-
@Test
- fun apiInterceptorTest() {
+ fun apiInterceptorTest() = runSessionTest(context()) { commonTestHelper ->
val responses = mutableListOf()
val listener = object : ApiInterceptorListener {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 0cb8003919..9160130bfa 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -54,12 +54,39 @@ import java.util.concurrent.TimeUnit
* This class exposes methods to be used in common cases
* Registration, login, Sync, Sending messages...
*/
-class CommonTestHelper(context: Context) {
+class CommonTestHelper private constructor(context: Context) {
+
+ companion object {
+ internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
+ val testHelper = CommonTestHelper(context)
+ return try {
+ block(testHelper)
+ } finally {
+ if (autoSignoutOnClose) {
+ testHelper.cleanUpOpenedSessions()
+ }
+ }
+ }
+
+ internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) {
+ val testHelper = CommonTestHelper(context)
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ return try {
+ block(cryptoTestHelper, testHelper)
+ } finally {
+ if (autoSignoutOnClose) {
+ testHelper.cleanUpOpenedSessions()
+ }
+ }
+ }
+ }
internal val matrix: TestMatrix
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var accountNumber = 0
+ private val trackedSessions = mutableListOf()
+
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
init {
@@ -84,6 +111,15 @@ class CommonTestHelper(context: Context) {
return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
}
+ fun cleanUpOpenedSessions() {
+ trackedSessions.forEach {
+ runBlockingTest {
+ it.signOutService().signOut(true)
+ }
+ }
+ trackedSessions.clear()
+ }
+
/**
* Create a homeserver configuration, with Http connection allowed for test
*/
@@ -248,7 +284,9 @@ class CommonTestHelper(context: Context) {
testParams
)
assertNotNull(session)
- return session
+ return session.also {
+ trackedSessions.add(session)
+ }
}
/**
@@ -266,7 +304,9 @@ class CommonTestHelper(context: Context) {
): Session {
val session = logAccountAndSync(userId, password, testParams)
assertNotNull(session)
- return session
+ return session.also {
+ trackedSessions.add(session)
+ }
}
/**
@@ -447,6 +487,7 @@ class CommonTestHelper(context: Context) {
fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) {
+ trackedSessions.remove(session)
runBlockingTest(timeout = 60_000) {
session.signOutService().signOut(true)
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index 348841313b..5fd86d4fdb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -58,7 +58,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
-import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
@@ -66,7 +66,7 @@ import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
-class CryptoTestHelper(private val testHelper: CommonTestHelper) {
+class CryptoTestHelper(val testHelper: CommonTestHelper) {
private val messagesFromAlice: List = listOf("0 - Hello I'm Alice!", "4 - Go!")
private val messagesFromBob: List = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.")
@@ -361,19 +361,19 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
// set up megolm backup
@@ -390,7 +390,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
index b16ab98e6c..39f49a9ccc 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt
@@ -40,6 +40,9 @@ class RetryTestRule(val retryCount: Int = 3) : TestRule {
for (i in 0 until retryCount) {
try {
base.evaluate()
+ if (i > 0) {
+ println("Retried test $i times")
+ }
return
} catch (t: Throwable) {
caughtThrowable = t
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
new file mode 100644
index 0000000000..a48b45a1f5
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class DecryptRedactedEventTest : InstrumentedTest {
+
+ @Test
+ fun doNotFailToDecryptRedactedEvent() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val e2eRoomID = testData.roomId
+ val aliceSession = testData.firstSession
+ val bobSession = testData.secondSession!!
+
+ val roomALicePOV = aliceSession.getRoom(e2eRoomID)!!
+ val timelineEvent = testHelper.sendTextMessage(roomALicePOV, "Hello", 1).first()
+ val redactionReason = "Wrong Room"
+ roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
+
+ // get the event from bob
+ testHelper.waitWithLatch {
+ testHelper.retryPeriodicallyWithLatch(it) {
+ bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
+ }
+ }
+
+ val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
+
+ testHelper.runBlockingTest {
+ try {
+ val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
+ Assert.assertEquals(
+ "Unexpected redacted reason",
+ redactionReason,
+ result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason")
+ )
+ Assert.assertEquals(
+ "Unexpected Redacted event id",
+ timelineEvent.eventId,
+ result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts
+ )
+ } catch (failure: Throwable) {
+ Assert.fail("Should not throw when decrypting a redacted event")
+ }
+ }
+ }
+}
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
index 3a8053dfa7..5a61eee7fe 100644
--- 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
@@ -23,6 +23,7 @@ import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -57,7 +58,8 @@ 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.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@@ -68,6 +70,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
+@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@@ -82,9 +85,7 @@ class E2eeSanityTests : InstrumentedTest {
* Alice sends a new message, then check that the new one can be decrypted
*/
@Test
- fun testSendingE2EEMessages() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testSendingE2EEMessages() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@@ -198,21 +199,12 @@ class E2eeSanityTests : InstrumentedTest {
}
}
}
-
- otherAccounts.forEach {
- testHelper.signOutAndClose(it)
- }
- newAccount.forEach { testHelper.signOutAndClose(it) }
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun testKeyGossipingIsEnabledByDefault() {
- val testHelper = CommonTestHelper(context())
+ fun testKeyGossipingIsEnabledByDefault() = runSessionTest(context()) { testHelper ->
val session = testHelper.createAccount("alice", SessionTestParams(true))
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
- testHelper.signOutAndClose(session)
}
/**
@@ -230,9 +222,7 @@ class E2eeSanityTests : InstrumentedTest {
* 9. Check that new session can decrypt
*/
@Test
- fun testBasicBackupImport() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testBasicBackupImport() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@@ -344,8 +334,6 @@ class E2eeSanityTests : InstrumentedTest {
// ensure bob can now decrypt
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
-
- testHelper.signOutAndClose(newBobSession)
}
/**
@@ -353,9 +341,7 @@ class E2eeSanityTests : InstrumentedTest {
* get them from an older one.
*/
@Test
- fun testSimpleGossip() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testSimpleGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@@ -449,18 +435,13 @@ class E2eeSanityTests : InstrumentedTest {
}
cryptoTestHelper.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 testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@@ -576,10 +557,6 @@ class E2eeSanityTests : InstrumentedTest {
canDecryptFirst && canDecryptSecond
}
}
-
- testHelper.signOutAndClose(aliceSession)
- testHelper.signOutAndClose(bobSessionWithBetterKey)
- testHelper.signOutAndClose(newBobSession)
}
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
@@ -610,9 +587,7 @@ class E2eeSanityTests : InstrumentedTest {
* Test that if a better key is forwared (lower index, it is then used)
*/
@Test
- fun testSelfInteractiveVerificationAndGossip() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
cryptoTestHelper.bootstrapSecurity(aliceSession)
@@ -751,9 +726,6 @@ class E2eeSanityTests : InstrumentedTest {
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
)
-
- testHelper.signOutAndClose(aliceSession)
- testHelper.signOutAndClose(aliceNewSession)
}
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt
index 65ba33cb02..2f2d54b7e3 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt
@@ -83,8 +83,8 @@ class ExportEncryptionTest {
@Test
fun checkExportDecrypt1() {
val password = "password"
- val input =
- "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" +
+ "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----"
val expectedString = "plain"
var decodedString: String? = null
@@ -104,8 +104,8 @@ class ExportEncryptionTest {
@Test
fun checkExportDecrypt2() {
val password = "betterpassword"
- val input =
- "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" + "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" +
+ "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----"
val expectedString = "Hello, World"
var decodedString: String? = null
@@ -125,8 +125,8 @@ class ExportEncryptionTest {
@Test
fun checkExportDecrypt3() {
val password = "SWORDFISH"
- val input =
- "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" + "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----"
+ val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" +
+ "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----"
val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically"
var decodedString: String? = null
@@ -205,8 +205,8 @@ class ExportEncryptionTest {
@Test
fun checkExportEncrypt4() {
- val password =
- "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword"
+ val password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
+ "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword"
val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically"
var decodedString: String? = null
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 93aa78a305..e37ae5be86 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
@@ -30,18 +30,14 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class PreShareKeysTest : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
-
@Test
- fun ensure_outbound_session_happy_path() {
+ fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
@@ -94,7 +90,5 @@ class PreShareKeysTest : InstrumentedTest {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
}
}
-
- testData.cleanUp(testHelper)
}
}
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 0f3a4b4181..5fe7376184 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
@@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
@@ -63,8 +62,6 @@ import kotlin.coroutines.resume
class UnwedgingTest : InstrumentedTest {
private lateinit var messagesReceivedByBob: List
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Before
fun init() {
@@ -85,7 +82,7 @@ class UnwedgingTest : InstrumentedTest {
* -> This is automatically fixed after SDKs restarted the olm session
*/
@Test
- fun testUnwedging() {
+ fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -240,8 +237,6 @@ class UnwedgingTest : InstrumentedTest {
}
bobTimeline.dispose()
-
- cryptoTestData.cleanUp(testHelper)
}
private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
index abcf1714b8..05790bfb7d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
@@ -37,8 +37,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import kotlin.coroutines.Continuation
@@ -49,11 +49,8 @@ import kotlin.coroutines.resume
@LargeTest
class XSigningTest : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
-
@Test
- fun test_InitializeAndStoreKeys() {
+ fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
testHelper.doSync {
@@ -87,7 +84,7 @@ class XSigningTest : InstrumentedTest {
}
@Test
- fun test_CrossSigningCheckBobSeesTheKeys() {
+ fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -137,12 +134,10 @@ class XSigningTest : InstrumentedTest {
)
assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted())
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun test_CrossSigningTestAliceTrustBobNewDevice() {
+ fun test_CrossSigningTestAliceTrustBobNewDevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -216,9 +211,5 @@ class XSigningTest : InstrumentedTest {
val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
-
- testHelper.signOutAndClose(aliceSession)
- testHelper.signOutAndClose(bobSession)
- testHelper.signOutAndClose(bobSession2)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
index 85b6c21df3..5f26fda946 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestHelper
import java.util.concurrent.CountDownLatch
@@ -42,35 +43,36 @@ import java.util.concurrent.CountDownLatch
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class EncryptionTest : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
-
@Test
fun test_EncryptionEvent() {
- performTest(roomShouldBeEncrypted = false) { room ->
- // Send an encryption Event as an Event (and not as a state event)
- room.sendService().sendEvent(
- eventType = EventType.STATE_ROOM_ENCRYPTION,
- content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
- )
- }
- }
-
- @Test
- fun test_EncryptionStateEvent() {
- performTest(roomShouldBeEncrypted = true) { room ->
- runBlocking {
- // Send an encryption Event as a State Event
- room.stateService().sendStateEvent(
+ runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room ->
+ // Send an encryption Event as an Event (and not as a state event)
+ room.sendService().sendEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
- stateKey = "",
- body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
+ content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
)
}
}
}
- private fun performTest(roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
+ @Test
+ fun test_EncryptionStateEvent() {
+ runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room ->
+ runBlocking {
+ // Send an encryption Event as a State Event
+ room.stateService().sendStateEvent(
+ eventType = EventType.STATE_ROOM_ENCRYPTION,
+ stateKey = "",
+ body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
+ )
+ }
+ }
+ }
+ }
+
+ private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
val aliceSession = cryptoTestData.firstSession
@@ -109,6 +111,5 @@ class EncryptionTest : InstrumentedTest {
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
it.countDown()
}
- cryptoTestData.cleanUp(testHelper)
}
}
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 49248292e6..b16e4b82eb 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
@@ -41,8 +41,7 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@@ -56,9 +55,7 @@ class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@Test
- fun test_DoNotSelfShareIfNotTrusted() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
@@ -194,9 +191,7 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@Test
- fun test_reShareIfWasIntendedToBeShared() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
@@ -227,9 +222,7 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@Test
- fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
val aliceSession = testData.firstSession
@@ -266,9 +259,7 @@ class KeyShareTests : InstrumentedTest {
* Tests that keys reshared with own verified session are done from the earliest known index
*/
@Test
- fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
@@ -388,10 +379,7 @@ class KeyShareTests : InstrumentedTest {
* Tests that we don't cancel a request to early on first forward if the index is not good enough
*/
@Test
- fun test_dontCancelToEarly() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
+ fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
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 13133b726c..0aac4297e4 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
@@ -36,8 +36,7 @@ import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.MockOkHttpInterceptor
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
@@ -52,9 +51,7 @@ class WithHeldTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@Test
- fun test_WithHeldUnverifiedReason() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_WithHeldUnverifiedReason() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
// =============================
// ARRANGE
@@ -153,16 +150,10 @@ class WithHeldTests : InstrumentedTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
}
-
- testHelper.signOutAndClose(aliceSession)
- testHelper.signOutAndClose(bobSession)
- testHelper.signOutAndClose(bobUnverifiedSession)
}
@Test
- fun test_WithHeldNoOlm() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
@@ -239,14 +230,10 @@ class WithHeldTests : InstrumentedTest {
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
aliceInterceptor.clearRules()
- testData.cleanUp(testHelper)
- testHelper.signOutAndClose(bobSecondSession)
}
@Test
- fun test_WithHeldKeyRequest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
@@ -293,8 +280,5 @@ class WithHeldTests : InstrumentedTest {
wc?.code == WithHeldCode.UNAUTHORISED
}
}
-
- testHelper.signOutAndClose(aliceSession)
- testHelper.signOutAndClose(bobSecondSession)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index 9136272b1e..c6e17e8c44 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -43,8 +44,9 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
+import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
import java.util.Collections
@@ -55,15 +57,15 @@ import java.util.concurrent.CountDownLatch
@LargeTest
class KeysBackupTest : InstrumentedTest {
+ @get:Rule val rule = RetryTestRule(3)
+
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
* - Check backup keys after having marked one as backed up
* - Reset keys backup markers
*/
@Test
- fun roomKeysTest_testBackupStore_ok() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -102,8 +104,7 @@ class KeysBackupTest : InstrumentedTest {
* Check that prepareKeysBackupVersionWithPassword returns valid data
*/
@Test
- fun prepareKeysBackupVersionTest() {
- val testHelper = CommonTestHelper(context())
+ fun prepareKeysBackupVersionTest() = runSessionTest(context()) { testHelper ->
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
@@ -113,7 +114,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.doSync {
keysBackup.prepareKeysBackupVersion(null, null, it)
@@ -125,16 +126,13 @@ class KeysBackupTest : InstrumentedTest {
assertNotNull(megolmBackupCreationInfo.recoveryKey)
stateObserver.stopAndCheckStates(null)
- testHelper.signOutAndClose(bobSession)
}
/**
* Test creating a keys backup version and check that createKeysBackupVersion() returns valid data
*/
@Test
- fun createKeysBackupVersionTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
cryptoTestHelper.initializeCrossSigning(bobSession)
@@ -143,13 +141,13 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.doSync {
keysBackup.prepareKeysBackupVersion(null, null, it)
}
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
// Create the version
val version = testHelper.doSync {
@@ -157,7 +155,7 @@ class KeysBackupTest : InstrumentedTest {
}
// Backup must be enable now
- assertTrue(keysBackup.isEnabled)
+ assertTrue(keysBackup.isEnabled())
// Check that it's signed with MSK
val versionResult = testHelper.doSync {
@@ -193,7 +191,6 @@ class KeysBackupTest : InstrumentedTest {
}
stateObserver.stopAndCheckStates(null)
- testHelper.signOutAndClose(bobSession)
}
/**
@@ -201,9 +198,7 @@ class KeysBackupTest : InstrumentedTest {
* - Check the backup completes
*/
@Test
- fun backupAfterCreateKeysBackupVersionTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -238,16 +233,13 @@ class KeysBackupTest : InstrumentedTest {
KeysBackupState.ReadyToBackUp
)
)
- cryptoTestData.cleanUp(testHelper)
}
/**
* Check that backupAllGroupSessions() returns valid data
*/
@Test
- fun backupAllGroupSessionsTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -281,7 +273,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
stateObserver.stopAndCheckStates(null)
- cryptoTestData.cleanUp(testHelper)
}
/**
@@ -293,9 +284,7 @@ class KeysBackupTest : InstrumentedTest {
* - Compare the decrypted megolm key with the original one
*/
@Test
- fun testEncryptAndDecryptKeysBackupData() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -330,7 +319,6 @@ class KeysBackupTest : InstrumentedTest {
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
stateObserver.stopAndCheckStates(null)
- cryptoTestData.cleanUp(testHelper)
}
/**
@@ -340,9 +328,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- fun restoreKeysBackupTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -428,9 +414,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- fun trustKeyBackupVersionTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@@ -441,8 +425,8 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device
testHelper.doSync {
@@ -458,7 +442,7 @@ class KeysBackupTest : InstrumentedTest {
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
- assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
+ assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync {
@@ -477,7 +461,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
- testData.cleanUp(testHelper)
}
/**
@@ -491,9 +474,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- fun trustKeyBackupVersionWithRecoveryKeyTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@@ -504,8 +485,8 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the recovery key
testHelper.doSync {
@@ -521,7 +502,7 @@ class KeysBackupTest : InstrumentedTest {
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
- assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
+ assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync {
@@ -540,7 +521,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
- testData.cleanUp(testHelper)
}
/**
@@ -552,9 +532,7 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
- fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@@ -565,8 +543,8 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong recovery key
val latch = CountDownLatch(1)
@@ -579,11 +557,10 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must still see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
stateObserver.stopAndCheckStates(null)
- testData.cleanUp(testHelper)
}
/**
@@ -597,9 +574,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- fun trustKeyBackupVersionWithPasswordTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
@@ -612,8 +587,8 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the password
testHelper.doSync {
@@ -629,7 +604,7 @@ class KeysBackupTest : InstrumentedTest {
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
- assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
+ assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync {
@@ -648,7 +623,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
- testData.cleanUp(testHelper)
}
/**
@@ -660,9 +634,7 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
- fun trustKeyBackupVersionWithWrongPasswordTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
@@ -676,8 +648,8 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong password
val latch = CountDownLatch(1)
@@ -690,11 +662,10 @@ class KeysBackupTest : InstrumentedTest {
// - The new device must still see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
- assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
- assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
+ assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
+ assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
stateObserver.stopAndCheckStates(null)
- testData.cleanUp(testHelper)
}
/**
@@ -704,9 +675,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -730,8 +699,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
-
- testData.cleanUp(testHelper)
}
/**
@@ -741,9 +708,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- fun testBackupWithPassword() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@@ -790,8 +755,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
-
- testData.cleanUp(testHelper)
}
/**
@@ -801,9 +764,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- fun restoreKeysBackupWithAWrongPasswordTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@@ -830,8 +791,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
-
- testData.cleanUp(testHelper)
}
/**
@@ -841,9 +800,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@@ -863,8 +820,6 @@ class KeysBackupTest : InstrumentedTest {
}
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
-
- testData.cleanUp(testHelper)
}
/**
@@ -874,9 +829,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -900,8 +853,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
-
- testData.cleanUp(testHelper)
}
/**
@@ -909,9 +860,7 @@ class KeysBackupTest : InstrumentedTest {
* - Check the returned KeysVersionResult is trusted
*/
@Test
- fun testIsKeysBackupTrusted() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@@ -945,7 +894,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
stateObserver.stopAndCheckStates(null)
- cryptoTestData.cleanUp(testHelper)
}
/**
@@ -957,9 +905,7 @@ class KeysBackupTest : InstrumentedTest {
* -> That must fail and her backup state must be WrongBackUpVersion
*/
@Test
- fun testBackupWhenAnotherBackupWasCreated() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@@ -969,7 +915,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
// Wait for keys backup to be finished
val latch0 = CountDownLatch(1)
@@ -993,7 +939,7 @@ class KeysBackupTest : InstrumentedTest {
// - Make alice back up her keys to her homeserver
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
- assertTrue(keysBackup.isEnabled)
+ assertTrue(keysBackup.isEnabled())
testHelper.await(latch0)
@@ -1012,11 +958,10 @@ class KeysBackupTest : InstrumentedTest {
testHelper.await(latch2)
// -> That must fail and her backup state must be WrongBackUpVersion
- assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state)
- assertFalse(keysBackup.isEnabled)
+ assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState())
+ assertFalse(keysBackup.isEnabled())
stateObserver.stopAndCheckStates(null)
- cryptoTestData.cleanUp(testHelper)
}
/**
@@ -1032,9 +977,7 @@ class KeysBackupTest : InstrumentedTest {
* -> It must success
*/
@Test
- fun testBackupAfterVerifyingADevice() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@@ -1069,7 +1012,7 @@ class KeysBackupTest : InstrumentedTest {
// - Try to backup all in aliceSession2, it must fail
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
- assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
+ assertFalse("Backup should not be enabled", keysBackup2.isEnabled())
val stateObserver2 = StateObserver(keysBackup2)
@@ -1088,8 +1031,8 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(isSuccessful)
// Backup state must be NotTrusted
- assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.state)
- assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
+ assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState())
+ assertFalse("Backup should not be enabled", keysBackup2.isEnabled())
// - Validate the old device from the new one
aliceSession2.cryptoService().setDeviceVerification(
@@ -1103,7 +1046,7 @@ class KeysBackupTest : InstrumentedTest {
keysBackup2.addListener(object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
// Check the backup completes
- if (keysBackup2.state == KeysBackupState.ReadyToBackUp) {
+ if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) {
// Remove itself from the list of listeners
keysBackup2.removeListener(this)
@@ -1121,12 +1064,10 @@ class KeysBackupTest : InstrumentedTest {
}
// -> It must success
- assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled)
+ assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled())
stateObserver.stopAndCheckStates(null)
stateObserver2.stopAndCheckStates(null)
- testHelper.signOutAndClose(aliceSession2)
- cryptoTestData.cleanUp(testHelper)
}
/**
@@ -1134,9 +1075,7 @@ class KeysBackupTest : InstrumentedTest {
* - Delete the backup
*/
@Test
- fun deleteKeysBackupTest() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@@ -1146,19 +1085,18 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
- assertTrue(keysBackup.isEnabled)
+ assertTrue(keysBackup.isEnabled())
// Delete the backup
testHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) }
// Backup is now disabled
- assertFalse(keysBackup.isEnabled)
+ assertFalse(keysBackup.isEnabled())
stateObserver.stopAndCheckStates(null)
- cryptoTestData.cleanUp(testHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
index 982817b8de..38f94c5103 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -109,7 +109,7 @@ internal class KeysBackupTestHelper(
Assert.assertNotNull(megolmBackupCreationInfo)
- Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled)
+ Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled())
// Create the version
val keysVersion = testHelper.doSync {
@@ -119,7 +119,7 @@ internal class KeysBackupTestHelper(
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
// Backup must be enable now
- Assert.assertTrue(keysBackup.isEnabled)
+ Assert.assertTrue(keysBackup.isEnabled())
stateObserver.stopAndCheckStates(null)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
@@ -131,7 +131,7 @@ internal class KeysBackupTestHelper(
*/
fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
// If already in the wanted state, return
- if (session.cryptoService().keysBackupService().state == state) {
+ if (session.cryptoService().keysBackupService().getState() == state) {
return
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
new file mode 100644
index 0000000000..53cf802b91
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.replayattack
+
+import androidx.test.filters.LargeTest
+import org.amshove.kluent.internal.assertFailsWith
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+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.crypto.MXCryptoError
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class ReplayAttackTest : InstrumentedTest {
+
+ @Test
+ fun replayAttackAlreadyDecryptedEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ val e2eRoomID = cryptoTestData.roomId
+
+ // Alice
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
+
+ // Bob
+ val bobSession = cryptoTestData.secondSession
+ val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
+ assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
+
+ // Alice will send a message
+ val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
+ assertEquals(1, sentEvents.size)
+
+ val fakeEventId = sentEvents[0].eventId + "_fake"
+ val fakeEventWithTheSameIndex =
+ sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
+
+ testHelper.runBlockingTest {
+ // Lets assume we are from the main timelineId
+ val timelineId = "timelineId"
+ // Lets decrypt the original event
+ aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
+ // Lets decrypt the fake event that will have the same message index
+ val exception = assertFailsWith {
+ // An exception should be thrown while the same index would have been used for the previous decryption
+ aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
+ }
+ assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
+ }
+ cryptoTestData.cleanUp(testHelper)
+ }
+
+ @Test
+ fun replayAttackSameEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ val e2eRoomID = cryptoTestData.roomId
+
+ // Alice
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
+
+ // Bob
+ val bobSession = cryptoTestData.secondSession
+ val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
+ assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
+
+ // Alice will send a message
+ val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
+ Assert.assertTrue("Message should be sent", sentEvents.size == 1)
+ assertEquals(sentEvents.size, 1)
+
+ testHelper.runBlockingTest {
+ // Lets assume we are from the main timelineId
+ val timelineId = "timelineId"
+ // Lets decrypt the original event
+ aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
+ try {
+ // Lets try to decrypt the same event
+ aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
+ } catch (ex: Throwable) {
+ fail("Shouldn't throw a decryption error for same event")
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
index c758050fc9..c8be6aae74 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
@@ -31,15 +31,16 @@ import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.KeySigner
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
-import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
@@ -55,8 +56,7 @@ class QuadSTests : InstrumentedTest {
}
@Test
- fun test_Generate4SKey() {
- val testHelper = CommonTestHelper(context())
+ fun test_Generate4SKey() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@@ -108,12 +108,11 @@ class QuadSTests : InstrumentedTest {
}
@Test
- fun test_StoreSecret() {
- val testHelper = CommonTestHelper(context())
+ fun test_StoreSecret() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId = "My.Key"
- val info = generatedSecret(aliceSession, keyId, true)
+ val info = generatedSecret(testHelper, aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
@@ -123,11 +122,11 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService().storeSecret(
"secret.of.life",
clearSecret,
- listOf(SharedSecretStorageService.KeyRef(null, keySpec)) // default key
+ listOf(KeyRef(null, keySpec)) // default key
)
}
- val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
+ val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent)
@@ -149,12 +148,10 @@ class QuadSTests : InstrumentedTest {
}
assertEquals("Secret mismatch", clearSecret, decryptedSecret)
- testHelper.signOutAndClose(aliceSession)
}
@Test
- fun test_SetDefaultLocalEcho() {
- val testHelper = CommonTestHelper(context())
+ fun test_SetDefaultLocalEcho() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@@ -170,19 +167,16 @@ class QuadSTests : InstrumentedTest {
testHelper.runBlockingTest {
quadS.setDefaultKey(TEST_KEY_ID)
}
-
- testHelper.signOutAndClose(aliceSession)
}
@Test
- fun test_StoreSecretWithMultipleKey() {
- val testHelper = CommonTestHelper(context())
+ fun test_StoreSecretWithMultipleKey() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
- val key1Info = generatedSecret(aliceSession, keyId1, true)
+ val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true)
val keyId2 = "Key2"
- val key2Info = generatedSecret(aliceSession, keyId2, true)
+ val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
@@ -191,8 +185,8 @@ class QuadSTests : InstrumentedTest {
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
listOf(
- SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
- SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
+ KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
+ KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
)
)
}
@@ -221,19 +215,16 @@ class QuadSTests : InstrumentedTest {
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
)
}
-
- testHelper.signOutAndClose(aliceSession)
}
@Test
@Ignore("Test is working locally, not in GitHub actions")
- fun test_GetSecretWithBadPassphrase() {
- val testHelper = CommonTestHelper(context())
+ fun test_GetSecretWithBadPassphrase() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val passphrase = "The good pass phrase"
- val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
+ val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
@@ -241,7 +232,7 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService().storeSecret(
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
- listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
+ listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
)
}
@@ -275,13 +266,9 @@ class QuadSTests : InstrumentedTest {
)
)
}
-
- testHelper.signOutAndClose(aliceSession)
}
- private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
- val testHelper = CommonTestHelper(context())
-
+ private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent {
var accountData: UserAccountDataEvent? = null
testHelper.waitWithLatch {
val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type)
@@ -297,29 +284,27 @@ class QuadSTests : InstrumentedTest {
return accountData!!
}
- private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
+ private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
- val testHelper = CommonTestHelper(context())
val creationInfo = testHelper.runBlockingTest {
quadS.generateKey(keyId, null, keyId, emptyKeySigner)
}
- assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
+ assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
- assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
+ assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo
}
- private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
+ private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
- val testHelper = CommonTestHelper(context())
val creationInfo = testHelper.runBlockingTest {
quadS.generateKeyWithPassphrase(
@@ -331,12 +316,12 @@ class QuadSTests : InstrumentedTest {
)
}
- assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
+ assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
- assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
+ assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
index a35c4a7418..bd0f9d1641 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
@@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
import org.matrix.android.sdk.internal.crypto.model.rest.toValue
@@ -56,9 +55,7 @@ import java.util.concurrent.CountDownLatch
class SASTest : InstrumentedTest {
@Test
- fun test_aliceStartThenAliceCancel() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -135,15 +132,11 @@ class SASTest : InstrumentedTest {
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID))
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID))
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
- fun test_key_agreement_protocols_must_include_curve25519() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@@ -195,15 +188,11 @@ class SASTest : InstrumentedTest {
testHelper.await(cancelLatch)
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
- fun test_key_agreement_macs_Must_include_hmac_sha256() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@@ -236,15 +225,11 @@ class SASTest : InstrumentedTest {
val cancelReq = canceledToDeviceEvent!!.content.toModel()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
- fun test_key_agreement_short_code_include_decimal() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@@ -277,8 +262,6 @@ class SASTest : InstrumentedTest {
val cancelReq = canceledToDeviceEvent!!.content.toModel()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
-
- cryptoTestData.cleanUp(testHelper)
}
private fun fakeBobStart(
@@ -316,9 +299,7 @@ class SASTest : InstrumentedTest {
// any two devices may only have at most one key verification in flight at a time.
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
@Test
- fun test_aliceStartTwoRequests() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -359,9 +340,7 @@ class SASTest : InstrumentedTest {
*/
@Test
@Ignore("This test will be ignored until it is fixed")
- fun test_aliceAndBobAgreement() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -415,14 +394,10 @@ class SASTest : InstrumentedTest {
accepted!!.shortAuthenticationStrings.forEach {
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
}
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun test_aliceAndBobSASCode() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -475,14 +450,10 @@ class SASTest : InstrumentedTest {
"Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
)
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun test_happyPath() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -555,13 +526,10 @@ class SASTest : InstrumentedTest {
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun test_ConcurrentStart() {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -648,7 +616,5 @@ class SASTest : InstrumentedTest {
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
}
-
- cryptoTestData.cleanUp(testHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index e2c2e153e1..265b7c8c4c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -31,8 +31,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch
@@ -156,9 +156,7 @@ class VerificationTest : InstrumentedTest {
bobSupportedMethods: List,
expectedResultForAlice: ExpectedResult,
expectedResultForBob: ExpectedResult
- ) {
- val testHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(testHelper)
+ ) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@@ -253,14 +251,11 @@ class VerificationTest : InstrumentedTest {
pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode
pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode
}
-
- cryptoTestData.cleanUp(testHelper)
}
@Test
- fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
+ fun test_selfVerificationAcceptedCancelsItForOtherSessions() = runSessionTest(context()) { testHelper ->
val defaultSessionParams = SessionTestParams(true)
- val testHelper = CommonTestHelper(context())
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index a2984dd27e..1ffcc2a3e6 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.timeline.Timeline
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.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@@ -44,9 +43,7 @@ import java.util.concurrent.CountDownLatch
class ThreadMessagingTest : InstrumentedTest {
@Test
- fun reply_in_thread_should_create_a_thread() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun reply_in_thread_should_create_a_thread() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
@@ -104,9 +101,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
- fun reply_in_thread_should_create_a_thread_from_other_user() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun reply_in_thread_should_create_a_thread_from_other_user() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@@ -179,9 +174,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
- fun reply_in_thread_to_timeline_message_multiple_times() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun reply_in_thread_to_timeline_message_multiple_times() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
@@ -244,9 +237,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
- fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
index 3c57cb8dd5..4b90849a1f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@@ -47,9 +46,7 @@ import java.util.concurrent.CountDownLatch
class PollAggregationTest : InstrumentedTest {
@Test
- fun testAllPollUseCases() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@@ -138,7 +135,6 @@ class PollAggregationTest : InstrumentedTest {
aliceSession.stopSync()
aliceTimeline.dispose()
- cryptoTestData.cleanUp(commonTestHelper)
}
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
index e407c1b42d..3dd3f5fa2a 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
@@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
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.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.checkSendOrder
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@@ -53,9 +52,7 @@ class TimelineForwardPaginationTest : InstrumentedTest {
* This test ensure that if we click to permalink, we will be able to go back to the live
*/
@Test
- fun forwardPaginationTest() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun forwardPaginationTest() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val numberOfMessagesToSend = 90
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
@@ -177,7 +174,5 @@ class TimelineForwardPaginationTest : InstrumentedTest {
}
aliceTimeline.dispose()
-
- cryptoTestData.cleanUp(commonTestHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
index 1a36adec44..3ff4572add 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
@@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
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.checkSendOrder
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@@ -48,9 +47,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
*/
@Test
- fun previousLastForwardTest() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun previousLastForwardTest() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@@ -242,7 +239,5 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
}
bobTimeline.dispose()
-
- cryptoTestData.cleanUp(commonTestHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
index 42f710d7cf..7ed0be927c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
@@ -32,8 +32,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
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.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants
@RunWith(JUnit4::class)
@@ -42,9 +41,7 @@ import org.matrix.android.sdk.common.TestConstants
class TimelineSimpleBackPaginationTest : InstrumentedTest {
@Test
- fun timeline_backPaginate_shouldReachEndOfTimeline() {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ fun timeline_backPaginate_shouldReachEndOfTimeline() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val numberOfMessagesToSent = 200
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
@@ -102,6 +99,5 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest {
assertEquals(numberOfMessagesToSent, onlySentEvents.size)
bobTimeline.dispose()
- cryptoTestData.cleanUp(commonTestHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
index 02430dda74..87f404b0f1 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
@@ -30,8 +30,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
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.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
/** !! Not working with the new timeline
@@ -47,15 +46,12 @@ class TimelineWithManyMembersTest : InstrumentedTest {
private const val NUMBER_OF_MEMBERS = 6
}
- private val commonTestHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
/**
* Ensures when someone sends a message to a crowded room, everyone can decrypt the message.
*/
@Test
- fun everyone_should_decrypt_message_in_a_crowded_room() {
+ fun everyone_should_decrypt_message_in_a_crowded_room() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS)
val sessionForFirstMember = cryptoTestData.firstSession
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
index e17b7efbd6..7c97426c39 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
@@ -26,9 +26,8 @@ import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.search.SearchResult
-import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestData
-import org.matrix.android.sdk.common.CryptoTestHelper
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@@ -74,9 +73,7 @@ class SearchMessagesTest : InstrumentedTest {
}
}
- private fun doTest(block: suspend (CryptoTestData) -> SearchResult) {
- val commonTestHelper = CommonTestHelper(context())
- val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+ private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
@@ -99,7 +96,5 @@ class SearchMessagesTest : InstrumentedTest {
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
}.orFalse()
)
-
- cryptoTestData.cleanUp(commonTestHelper)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
index b9760c1bfc..0d8a9058a2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
@@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
-import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@@ -49,8 +49,7 @@ import org.matrix.android.sdk.common.SessionTestParams
class SpaceCreationTest : InstrumentedTest {
@Test
- fun createSimplePublicSpace() {
- val commonTestHelper = CommonTestHelper(context())
+ fun createSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
val roomName = "My Space"
val topic = "A public space for test"
@@ -96,13 +95,10 @@ class SpaceCreationTest : InstrumentedTest {
?.toModel()?.historyVisibility
assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility)
-
- commonTestHelper.signOutAndClose(session)
}
@Test
- fun testJoinSimplePublicSpace() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testJoinSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
@@ -134,8 +130,7 @@ class SpaceCreationTest : InstrumentedTest {
}
@Test
- fun testSimplePublicSpaceWithChildren() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testSimplePublicSpaceWithChildren() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
@@ -204,8 +199,5 @@ class SpaceCreationTest : InstrumentedTest {
// ).size
//
// assertEquals("Unexpected number of joined children", 1, childCount)
-
- commonTestHelper.signOutAndClose(aliceSession)
- commonTestHelper.signOutAndClose(bobSession)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index b72618c3bd..5396251438 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -29,8 +29,8 @@ 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.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.SpaceFilter
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.events.model.toContent
@@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@@ -55,8 +56,7 @@ import org.matrix.android.sdk.common.SessionTestParams
class SpaceHierarchyTest : InstrumentedTest {
@Test
- fun createCanonicalChildRelation() {
- val commonTestHelper = CommonTestHelper(context())
+ fun createCanonicalChildRelation() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceName = "My Space"
@@ -173,8 +173,7 @@ class SpaceHierarchyTest : InstrumentedTest {
// }
@Test
- fun testFilteringBySpace() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@@ -185,12 +184,12 @@ class SpaceHierarchyTest : InstrumentedTest {
)
/* val spaceBInfo = */ createPublicSpace(
- session, "SpaceB", listOf(
- Triple("B1", true /*auto-join*/, true/*canonical*/),
- Triple("B2", true, true),
- Triple("B3", true, true)
- )
- )
+ session, "SpaceB", listOf(
+ Triple("B1", true /*auto-join*/, true/*canonical*/),
+ Triple("B2", true, true),
+ Triple("B3", true, true)
+ )
+ )
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
@@ -249,15 +248,14 @@ class SpaceHierarchyTest : InstrumentedTest {
Thread.sleep(6_000)
val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams {
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
+ spaceFilter = SpaceFilter.OrphanRooms
})
assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size)
}
@Test
@Ignore("This test will be ignored until it is fixed")
- fun testBreakCycle() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@@ -302,8 +300,7 @@ class SpaceHierarchyTest : InstrumentedTest {
}
@Test
- fun testLiveFlatChildren() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@@ -396,25 +393,26 @@ class SpaceHierarchyTest : InstrumentedTest {
childInfo: List>
/** Name, auto-join, canonical*/
): TestSpaceCreationResult {
- val commonTestHelper = CommonTestHelper(context())
var spaceId = ""
var roomIds: List = emptyList()
- commonTestHelper.waitWithLatch { latch ->
- spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
- val syncedSpace = session.spaceService().getSpace(spaceId)
- val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+ runSessionTest(context()) { commonTestHelper ->
+ commonTestHelper.waitWithLatch { latch ->
+ spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
+ val syncedSpace = session.spaceService().getSpace(spaceId)
+ val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
- roomIds = childInfo.map { entry ->
- session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
- }
- roomIds.forEachIndexed { index, roomId ->
- syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
- val canonical = childInfo[index].third
- if (canonical != null) {
- session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
+ roomIds = childInfo.map { entry ->
+ session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
}
+ roomIds.forEachIndexed { index, roomId ->
+ syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+ val canonical = childInfo[index].third
+ if (canonical != null) {
+ session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
+ }
+ }
+ latch.countDown()
}
- latch.countDown()
}
return TestSpaceCreationResult(spaceId, roomIds)
}
@@ -425,51 +423,51 @@ class SpaceHierarchyTest : InstrumentedTest {
childInfo: List>
/** Name, auto-join, canonical*/
): TestSpaceCreationResult {
- val commonTestHelper = CommonTestHelper(context())
var spaceId = ""
var roomIds: List = emptyList()
- commonTestHelper.waitWithLatch { latch ->
- spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
- val syncedSpace = session.spaceService().getSpace(spaceId)
- val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
- roomIds =
- childInfo.map { entry ->
- val homeServerCapabilities = session
- .homeServerCapabilitiesService()
- .getHomeServerCapabilities()
- session.roomService().createRoom(CreateRoomParams().apply {
- name = entry.first
- this.featurePreset = RestrictedRoomPreset(
- homeServerCapabilities,
- listOf(
- RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
- )
- )
- })
+ runSessionTest(context()) { commonTestHelper ->
+ commonTestHelper.waitWithLatch { latch ->
+ spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
+ val syncedSpace = session.spaceService().getSpace(spaceId)
+ val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+ roomIds =
+ childInfo.map { entry ->
+ val homeServerCapabilities = session
+ .homeServerCapabilitiesService()
+ .getHomeServerCapabilities()
+ session.roomService().createRoom(CreateRoomParams().apply {
+ name = entry.first
+ this.featurePreset = RestrictedRoomPreset(
+ homeServerCapabilities,
+ listOf(
+ RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
+ )
+ )
+ })
+ }
+ roomIds.forEachIndexed { index, roomId ->
+ syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+ val canonical = childInfo[index].third
+ if (canonical != null) {
+ session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
- roomIds.forEachIndexed { index, roomId ->
- syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
- val canonical = childInfo[index].third
- if (canonical != null) {
- session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
+ latch.countDown()
}
- latch.countDown()
}
return TestSpaceCreationResult(spaceId, roomIds)
}
@Test
- fun testRootSpaces() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testRootSpaces() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
/* val spaceAInfo = */ createPublicSpace(
- session, "SpaceA", listOf(
- Triple("A1", true /*auto-join*/, true/*canonical*/),
- Triple("A2", true, true)
- )
- )
+ session, "SpaceA", listOf(
+ Triple("A1", true /*auto-join*/, true/*canonical*/),
+ Triple("A2", true, true)
+ )
+ )
val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf(
@@ -508,13 +506,10 @@ class SpaceHierarchyTest : InstrumentedTest {
}
assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
-
- commonTestHelper.signOutAndClose(session)
}
@Test
- fun testParentRelation() {
- val commonTestHelper = CommonTestHelper(context())
+ fun testParentRelation() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
@@ -606,8 +601,5 @@ class SpaceHierarchyTest : InstrumentedTest {
bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
}
}
-
- commonTestHelper.signOutAndClose(aliceSession)
- commonTestHelper.signOutAndClose(bobSession)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
index a34dbcc196..3c376b55ee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
@@ -16,6 +16,14 @@
package org.matrix.android.sdk.api
+/**
+ * This interface exists to let the implementation provide localized room display name fallback.
+ * The methods can be called when the room has no name, i.e. its `m.room.name` state event does not exist or
+ * the name in it is an empty String.
+ * It allows the SDK to store the room name fallback into the local storage and so let the client do
+ * queries on the room name.
+ * *Limitation*: if the locale of the device changes, the methods will not be called again.
+ */
interface RoomDisplayNameFallbackProvider {
fun getNameForRoomInvite(): String
fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
index af27b5fbff..5ae70e1978 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
@@ -28,9 +28,11 @@ import org.matrix.android.sdk.api.session.Session
* This interface defines methods to authenticate or to create an account to a matrix server.
*/
interface AuthenticationService {
+
/**
* Request the supported login flows for this homeserver.
- * This is the first method to call to be able to get a wizard to login or to create an account
+ * This is the first method to call to be able to get a wizard to login or to create an account.
+ * @param homeServerConnectionConfig contains the homeserver URL to login to, a wellKnown lookup will be attempted.
*/
suspend fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult
@@ -66,7 +68,7 @@ interface AuthenticationService {
/**
* True when login and password has been sent with success to the homeserver.
*/
- val isRegistrationStarted: Boolean
+ fun isRegistrationStarted(): Boolean
/**
* Cancel pending login or pending registration.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt
index 439b4beb41..9e6b2b3ad9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt
@@ -18,13 +18,31 @@ package org.matrix.android.sdk.api.auth.registration
import org.matrix.android.sdk.api.session.Session
-// Either a session or an object containing data about registration stages
+/**
+ * Either a session or an object containing data about registration stages.
+ */
sealed class RegistrationResult {
+ /**
+ * The registration is successful, the [Session] is provided.
+ */
data class Success(val session: Session) : RegistrationResult()
+
+ /**
+ * The registration still miss some steps. See [FlowResult] to know the details.
+ */
data class FlowResponse(val flowResult: FlowResult) : RegistrationResult()
}
+/**
+ * Information about the missing and completed [Stage].
+ */
data class FlowResult(
+ /**
+ * List of missing stages.
+ */
val missingStages: List,
+ /**
+ * List of completed stages.
+ */
val completedStages: List
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
index 01e068af75..995fd27ace 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
@@ -111,14 +111,14 @@ interface RegistrationWizard {
suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult
/**
- * This is the current ThreePid, waiting for validation. The SDK will store it in database, so it can be
+ * Returns the current ThreePid, waiting for validation. The SDK will store it in database, so it can be
* restored even if the app has been killed during the registration
*/
- val currentThreePid: String?
+ fun getCurrentThreePid(): String?
/**
- * True when login and password have been sent with success to the homeserver, i.e. [createAccount] has been
+ * Return true when login and password have been sent with success to the homeserver, i.e. [createAccount] has been
* called successfully.
*/
- val isRegistrationStarted: Boolean
+ fun isRegistrationStarted(): Boolean
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt
index c21b667cf7..281b0c2808 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt
@@ -16,25 +16,40 @@
package org.matrix.android.sdk.api.auth.registration
+/**
+ * Registration stages.
+ */
sealed class Stage(open val mandatory: Boolean) {
- // m.login.recaptcha
+ /**
+ * m.login.recaptcha stage.
+ */
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
- // m.login.email.identity
+ /**
+ * m.login.email.identity stage.
+ */
data class Email(override val mandatory: Boolean) : Stage(mandatory)
- // m.login.msisdn
+ /**
+ * m.login.msisdn stage.
+ */
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
- // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
- // and a password, the dummy stage has to be done
+ /**
+ * m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
+ * and a password, the dummy stage has to be done.
+ */
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
- // Undocumented yet: m.login.terms
+ /**
+ * Undocumented yet: m.login.terms stage.
+ */
data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory)
- // For unknown stages
+ /**
+ * For unknown stages.
+ */
data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt
index 2880d851d6..ddf76d6e42 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt
@@ -17,13 +17,19 @@
package org.matrix.android.sdk.api.cache
sealed class CacheStrategy {
- // Data is always fetched from the server
+ /**
+ * Data is always fetched from the server.
+ */
object NoCache : CacheStrategy()
- // Once data is retrieved, it is stored for the provided amount of time.
- // In case of error, and if strict is set to false, the cache can be returned if available
+ /**
+ * Once data is retrieved, it is stored for the provided amount of time.
+ * In case of error, and if strict is set to false, the cache can be returned if available
+ */
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy()
- // Once retrieved, the data is stored in cache and will be always get from the cache
+ /**
+ * Once retrieved, the data is stored in cache and will be always get from the cache.
+ */
object InfiniteCache : CacheStrategy()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
index be139fd82b..7d4f553bed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
@@ -37,7 +37,9 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
- // When server send an error, but it cannot be interpreted as a MatrixError
+ /**
+ * When server send an error, but it cannot be interpreted as a MatrixError.
+ */
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
index 368ff98661..f08c86885d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
@@ -20,20 +20,52 @@ package org.matrix.android.sdk.api.query
* Basic query language. All these cases are mutually exclusive.
*/
sealed interface QueryStringValue {
+ /**
+ * No condition, i.e. there will be no test on the tested field.
+ */
+ object NoCondition : QueryStringValue
+
+ /**
+ * The tested field has to be null.
+ */
+ object IsNull : QueryStringValue
+
+ /**
+ * The tested field has to be not null.
+ */
+ object IsNotNull : QueryStringValue
+
+ /**
+ * The tested field has to be empty.
+ */
+ object IsEmpty : QueryStringValue
+
+ /**
+ * The tested field has to not empty.
+ */
+ object IsNotEmpty : QueryStringValue
+
+ /**
+ * Interface to check String content.
+ */
sealed interface ContentQueryStringValue : QueryStringValue {
val string: String
val case: Case
}
- object NoCondition : QueryStringValue
- object IsNull : QueryStringValue
- object IsNotNull : QueryStringValue
- object IsEmpty : QueryStringValue
- object IsNotEmpty : QueryStringValue
-
+ /**
+ * The tested field must match the [string].
+ */
data class Equals(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
+
+ /**
+ * The tested field must contain the [string].
+ */
data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue
+ /**
+ * Case enum for [ContentQueryStringValue].
+ */
enum class Case {
/**
* Match query sensitive to case.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt
index c8ccc4c8a3..c2117adbd3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt
@@ -16,9 +16,23 @@
package org.matrix.android.sdk.api.query
+/**
+ * To filter by Room category.
+ * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
+ */
enum class RoomCategoryFilter {
+ /**
+ * Get only the DM, i.e. the rooms referenced in `m.direct` account data.
+ */
ONLY_DM,
+
+ /**
+ * Get only the Room, not the DM, i.e. the rooms not referenced in `m.direct` account data.
+ */
ONLY_ROOMS,
+
+ /**
+ * Get the room with non-0 notifications.
+ */
ONLY_WITH_NOTIFICATIONS,
- ALL
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt
index 613916bc18..73947f8f7a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt
@@ -16,8 +16,22 @@
package org.matrix.android.sdk.api.query
+/**
+ * Filter room by their tag.
+ * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
+ * @see [org.matrix.android.sdk.api.session.room.model.tag.RoomTag]
+ */
data class RoomTagQueryFilter(
+ /**
+ * Set to true to get the rooms which have the tag "m.favourite".
+ */
val isFavorite: Boolean?,
+ /**
+ * Set to true to get the rooms which have the tag "m.lowpriority".
+ */
val isLowPriority: Boolean?,
- val isServerNotice: Boolean?
+ /**
+ * Set to true to get the rooms which have the tag "m.server_notice".
+ */
+ val isServerNotice: Boolean?,
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt
new file mode 100644
index 0000000000..6383412ffb
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.query
+
+/**
+ * Filter to be used to do room queries regarding the space hierarchy.
+ * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams]
+ */
+sealed interface SpaceFilter {
+ /**
+ * Used to get all the rooms that are not in any space.
+ */
+ object OrphanRooms : SpaceFilter
+
+ /**
+ * Used to get all the rooms that have the provided space in their parent hierarchy.
+ */
+ data class ActiveSpace(val spaceId: String) : SpaceFilter
+
+ /**
+ * Used to get all the rooms that do not have the provided space in their parent hierarchy.
+ */
+ data class ExcludeSpace(val spaceId: String) : SpaceFilter
+}
+
+/**
+ * Return a [SpaceFilter.ActiveSpace] if the String is not null, or [SpaceFilter.OrphanRooms].
+ */
+fun String?.toActiveSpaceOrOrphanRooms(): SpaceFilter = this?.let { SpaceFilter.ActiveSpace(it) } ?: SpaceFilter.OrphanRooms
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
index a0b1604e86..8745003f9f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
@@ -186,8 +186,6 @@ interface KeysBackupService {
callback: MatrixCallback
)
- fun onSecretKeyGossip(secret: String)
-
/**
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
*
@@ -200,7 +198,8 @@ interface KeysBackupService {
*/
fun restoreKeysWithRecoveryKey(
keysVersionResult: KeysVersionResult,
- recoveryKey: String, roomId: String?,
+ recoveryKey: String,
+ roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback
@@ -226,10 +225,13 @@ interface KeysBackupService {
)
val keysBackupVersion: KeysVersionResult?
+
val currentBackupVersion: String?
- val isEnabled: Boolean
- val isStucked: Boolean
- val state: KeysBackupState
+ get() = keysBackupVersion?.version
+
+ fun isEnabled(): Boolean
+ fun isStuck(): Boolean
+ fun getState(): KeysBackupState
// For gossiping
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt
index a4cc133398..a867d573de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt
@@ -51,33 +51,51 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup
*
*/
enum class KeysBackupState {
- // Need to check the current backup version on the homeserver
+ /**
+ * Need to check the current backup version on the homeserver.
+ */
Unknown,
- // Checking if backup is enabled on homeserver
+ /**
+ * Checking if backup is enabled on homeserver.
+ */
CheckingBackUpOnHomeserver,
- // Backup has been stopped because a new backup version has been detected on the homeserver
+ /**
+ * Backup has been stopped because a new backup version has been detected on the homeserver.
+ */
WrongBackUpVersion,
- // Backup from this device is not enabled
+ /**
+ * Backup from this device is not enabled.
+ */
Disabled,
- // There is a backup available on the homeserver but it is not trusted.
- // It is not trusted because the signature is invalid or the device that created it is not verified
- // Use [KeysBackup.getKeysBackupTrust()] to get trust details.
- // Consequently, the backup from this device is not enabled.
+ /**
+ * There is a backup available on the homeserver but it is not trusted.
+ * It is not trusted because the signature is invalid or the device that created it is not verified.
+ * Use [KeysBackup.getKeysBackupTrust()] to get trust details.
+ * Consequently, the backup from this device is not enabled.
+ */
NotTrusted,
- // Backup is being enabled: the backup version is being created on the homeserver
+ /**
+ * Backup is being enabled: the backup version is being created on the homeserver.
+ */
Enabling,
- // Backup is enabled and ready to send backup to the homeserver
+ /**
+ * Backup is enabled and ready to send backup to the homeserver.
+ */
ReadyToBackUp,
- // e2e keys are going to be sent to the homeserver
+ /**
+ * e2e keys are going to be sent to the homeserver.
+ */
WillBackUp,
- // e2e keys are being sent to the homeserver
+ /**
+ * e2e keys are being sent to the homeserver.
+ */
BackingUp
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
index 68c7496d58..78724819a3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
@@ -20,15 +20,23 @@ package org.matrix.android.sdk.api.session.crypto.model
* RoomEncryptionTrustLevel represents the trust level in an encrypted room.
*/
enum class RoomEncryptionTrustLevel {
- // No one in the room has been verified -> Black shield
+ /**
+ * No one in the room has been verified -> Black shield.
+ */
Default,
- // There are one or more device un-verified -> the app should display a red shield
+ /**
+ * There are one or more device un-verified -> the app should display a red shield.
+ */
Warning,
- // All devices in the room are verified -> the app should display a green shield
+ /**
+ * All devices in the room are verified -> the app should display a green shield.
+ */
Trusted,
- // e2e is active but with an unsupported algorithm
+ /**
+ * e2e is active but with an unsupported algorithm.
+ */
E2EWithUnsupportedAlgorithm
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt
index f2de2c4b47..0ab47a2ecd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt
@@ -20,12 +20,18 @@ package org.matrix.android.sdk.api.session.crypto.verification
* Verification methods.
*/
enum class VerificationMethod {
- // Use it when your application supports the SAS verification method
+ /**
+ * Use it when your application supports the SAS verification method.
+ */
SAS,
- // Use it if your application is able to display QR codes
+ /**
+ * Use it if your application is able to display QR codes.
+ */
QR_CODE_SHOW,
- // Use it if your application is able to scan QR codes
+ /**
+ * Use it if your application is able to scan QR codes.
+ */
QR_CODE_SCAN
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt
index 39de2cc712..30e4c66937 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt
@@ -17,10 +17,14 @@
package org.matrix.android.sdk.api.session.crypto.verification
sealed class VerificationTxState {
- // Uninitialized state
+ /**
+ * Uninitialized state.
+ */
object None : VerificationTxState()
- // Specific for SAS
+ /**
+ * Specific for SAS.
+ */
abstract class VerificationSasTxState : VerificationTxState()
object SendingStart : VerificationSasTxState()
@@ -38,18 +42,26 @@ sealed class VerificationTxState {
object MacSent : VerificationSasTxState()
object Verifying : VerificationSasTxState()
- // Specific for QR code
+ /**
+ * Specific for QR code.
+ */
abstract class VerificationQrTxState : VerificationTxState()
- // Will be used to ask the user if the other user has correctly scanned
+ /**
+ * Will be used to ask the user if the other user has correctly scanned.
+ */
object QrScannedByOther : VerificationQrTxState()
object WaitingOtherReciprocateConfirm : VerificationQrTxState()
- // Terminal states
+ /**
+ * Terminal states.
+ */
abstract class TerminalTxState : VerificationTxState()
object Verified : TerminalTxState()
- // Cancelled by me or by other
+ /**
+ * Cancelled by me or by other.
+ */
data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt
new file mode 100644
index 0000000000..1ae23e2b70
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.pushers
+
+data class HttpPusher(
+ /**
+ * This is a unique identifier for this pusher. The value you should use for
+ * this is the routing or destination address information for the notification,
+ * for example, the APNS token for APNS or the Registration ID for GCM. If your
+ * notification client has no such concept, use any unique identifier. Max length, 512 chars.
+ */
+ val pushkey: String,
+
+ /**
+ * The application id
+ * This is a reverse-DNS style identifier for the application. It is recommended
+ * that this end with the platform, such that different platform versions get
+ * different app identifiers. Max length, 64 chars.
+ */
+ val appId: String,
+
+ /**
+ * This string determines which set of device specific rules this pusher executes.
+ */
+ val profileTag: String,
+
+ /**
+ * The preferred language for receiving notifications (e.g. "en" or "en-US").
+ */
+ val lang: String,
+
+ /**
+ * A human readable string that will allow the user to identify what application owns this pusher.
+ */
+ val appDisplayName: String,
+
+ /**
+ * A human readable string that will allow the user to identify what device owns this pusher.
+ */
+ val deviceDisplayName: String,
+
+ /**
+ * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
+ */
+ val url: String,
+
+ /**
+ * If true, the homeserver should add another pusher with the given pushkey and App ID in addition
+ * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
+ * with the same App ID and pushkey for different users.
+ */
+ val append: Boolean,
+
+ /**
+ * true to limit the push content to only id and not message content
+ * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
+ */
+ val withEventIdOnly: Boolean
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
index 379f947b89..d7958ea3cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
@@ -111,61 +111,4 @@ interface PushersService {
* Get the current pushers.
*/
fun getPushers(): List
-
- data class HttpPusher(
-
- /**
- * This is a unique identifier for this pusher. The value you should use for
- * this is the routing or destination address information for the notification,
- * for example, the APNS token for APNS or the Registration ID for GCM. If your
- * notification client has no such concept, use any unique identifier. Max length, 512 chars.
- */
- val pushkey: String,
-
- /**
- * The application id
- * This is a reverse-DNS style identifier for the application. It is recommended
- * that this end with the platform, such that different platform versions get
- * different app identifiers. Max length, 64 chars.
- */
- val appId: String,
-
- /**
- * This string determines which set of device specific rules this pusher executes.
- */
- val profileTag: String,
-
- /**
- * The preferred language for receiving notifications (e.g. "en" or "en-US").
- */
- val lang: String,
-
- /**
- * A human readable string that will allow the user to identify what application owns this pusher.
- */
- val appDisplayName: String,
-
- /**
- * A human readable string that will allow the user to identify what device owns this pusher.
- */
- val deviceDisplayName: String,
-
- /**
- * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
- */
- val url: String,
-
- /**
- * If true, the homeserver should add another pusher with the given pushkey and App ID in addition
- * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
- * with the same App ID and pushkey for different users.
- */
- val append: Boolean,
-
- /**
- * true to limit the push content to only id and not message content
- * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
- */
- val withEventIdOnly: Boolean
- )
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index 3a18cf1497..5d2769ac3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic
import org.matrix.android.sdk.api.session.room.alias.AliasService
import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
+import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
@@ -163,4 +164,9 @@ interface Room {
* Get the RoomVersionService associated to this Room.
*/
fun roomVersionService(): RoomVersionService
+
+ /**
+ * Get the LocationSharingService associated to this Room.
+ */
+ fun locationSharingService(): LocationSharingService
}
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 67c5b21390..0a495f3552 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
@@ -99,6 +99,12 @@ interface RoomService {
*/
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
+ /**
+ * A live [RoomSummary] associated with the room with id [roomId].
+ * You can observe this summary to get dynamic data from this room, even if the room is not joined yet
+ */
+ fun getRoomSummaryLive(roomId: String): LiveData>
+
/**
* Get a snapshot list of room summaries.
* @return the immutable list of [RoomSummary]
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt
index e721abd6a0..9368ad6bf4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt
@@ -16,9 +16,28 @@
package org.matrix.android.sdk.api.session.room
+/**
+ * Enum to sort room list.
+ */
enum class RoomSortOrder {
+ /**
+ * Sort room list by room ascending name.
+ */
NAME,
+
+ /**
+ * Sort room list by room descending last activity.
+ */
ACTIVITY,
+
+ /**
+ * Sort room list by room priority and last activity: favorite room first, low priority room last,
+ * then descending last activity.
+ */
PRIORITY_AND_ACTIVITY,
+
+ /**
+ * Do not sort room list. Useful if the order does not matter. Order can be indeterminate.
+ */
NONE
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
index 5c74dcced1..3d943473e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
@@ -16,60 +16,99 @@
package org.matrix.android.sdk.api.session.room
-import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
+import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
+/**
+ * Create a [RoomSummaryQueryParams] object, calling [init] with a [RoomSummaryQueryParams.Builder].
+ */
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
- return RoomSummaryQueryParams.Builder().apply(init).build()
-}
-
-fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
return RoomSummaryQueryParams.Builder()
.apply(init)
- .apply {
- includeType = listOf(RoomType.SPACE)
- excludeType = null
- roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
- }
.build()
}
/**
- * This class can be used to filter room summaries to use with:
- * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService].
+ * Create a [SpaceSummaryQueryParams] object (which is a [RoomSummaryQueryParams]), calling [init] with a [RoomSummaryQueryParams.Builder].
+ * This is specific for spaces, other filters will be applied after invoking [init]
+ */
+fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
+ return roomSummaryQueryParams {
+ init()
+ includeType = listOf(RoomType.SPACE)
+ excludeType = null
+ roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
+ }
+}
+
+/**
+ * This class can be used to filter room summaries to use with [RoomService].
+ * It provides a [Builder].
+ * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of this class.
*/
data class RoomSummaryQueryParams(
- val roomId: QueryStringValue,
+ /**
+ * Query for the displayName of the room. The display name can be the value of the state event,
+ * or a value returned by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider].
+ */
val displayName: QueryStringValue,
+ /**
+ * Query for the canonical alias of the room.
+ */
val canonicalAlias: QueryStringValue,
+ /**
+ * Used to filter room by membership.
+ */
val memberships: List,
+ /**
+ * Used to filter room by room category.
+ */
val roomCategoryFilter: RoomCategoryFilter?,
+ /**
+ * Used to filter room by room tag.
+ */
val roomTagQueryFilter: RoomTagQueryFilter?,
+ /**
+ * Used to filter room by room type.
+ * @see [includeType]
+ */
val excludeType: List?,
+ /**
+ * Used to filter room by room type.
+ * @see [excludeType]
+ */
val includeType: List?,
- val activeSpaceFilter: ActiveSpaceFilter?,
+ /**
+ * Used to filter room using the current space.
+ */
+ val spaceFilter: SpaceFilter?,
+ /**
+ * Used to filter room using the current group.
+ */
val activeGroupId: String? = null
) {
+ /**
+ * Builder for [RoomSummaryQueryParams].
+ * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of [RoomSummaryQueryParams].
+ */
class Builder {
- var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
- var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
+ var displayName: QueryStringValue = QueryStringValue.NoCondition
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List = Membership.all()
- var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
+ var roomCategoryFilter: RoomCategoryFilter? = null
var roomTagQueryFilter: RoomTagQueryFilter? = null
var excludeType: List? = listOf(RoomType.SPACE)
var includeType: List? = null
- var activeSpaceFilter: ActiveSpaceFilter = ActiveSpaceFilter.None
+ var spaceFilter: SpaceFilter? = null
var activeGroupId: String? = null
fun build() = RoomSummaryQueryParams(
- roomId = roomId,
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships,
@@ -77,7 +116,7 @@ data class RoomSummaryQueryParams(
roomTagQueryFilter = roomTagQueryFilter,
excludeType = excludeType,
includeType = includeType,
- activeSpaceFilter = activeSpaceFilter,
+ spaceFilter = spaceFilter,
activeGroupId = activeGroupId
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
new file mode 100644
index 0000000000..dd48d51f45
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.location
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+
+/**
+ * Manage all location sharing related features.
+ */
+interface LocationSharingService {
+ fun getRunningLiveLocationShareSummaries(): LiveData>
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index 71c1d8303e..1ab23b7a11 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -28,65 +28,200 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
* It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
*/
data class RoomSummary(
+ /**
+ * The roomId of the room.
+ */
val roomId: String,
- // Computed display name
+ /**
+ * Computed display name. The value of the state event `m.room.name` if not empty, else can be the value returned
+ * by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider].
+ */
val displayName: String = "",
+ /**
+ * The value of the live state event `m.room.name`.
+ */
val name: String = "",
+ /**
+ * The value of the live state event `m.room.topic`.
+ */
val topic: String = "",
+ /**
+ * The value of the live state event `m.room.avatar`.
+ */
val avatarUrl: String = "",
+ /**
+ * The value of the live state event `m.room.canonical_alias`.
+ */
val canonicalAlias: String? = null,
+ /**
+ * The list of all the aliases of this room. Content of the live state event `m.room.aliases`.
+ */
val aliases: List = emptyList(),
+ /**
+ * The value of the live state event `m.room.join_rules`.
+ */
val joinRules: RoomJoinRules? = null,
+ /**
+ * True is this room is referenced in the account data `m.direct`.
+ */
val isDirect: Boolean = false,
+ /**
+ * If [isDirect] is true, this is the id of the first other member of this room.
+ */
val directUserId: String? = null,
+ /**
+ * If [isDirect] is true, this it the presence of the first other member of this room.
+ */
val directUserPresence: UserPresence? = null,
+ /**
+ * Number of members who have joined this room.
+ */
val joinedMembersCount: Int? = 0,
+ /**
+ * Number of members who are invited to this room.
+ */
val invitedMembersCount: Int? = 0,
+ /**
+ * Latest [TimelineEvent] which can be displayed in this room. Can be used in the room list.
+ */
val latestPreviewableEvent: TimelineEvent? = null,
+ /**
+ * List of other member ids of this room.
+ */
val otherMemberIds: List = emptyList(),
+ /**
+ * Number of unread message in this room.
+ */
val notificationCount: Int = 0,
+ /**
+ * Number of unread and highlighted message in this room.
+ */
val highlightCount: Int = 0,
+ /**
+ * True if this room has unread messages.
+ */
val hasUnreadMessages: Boolean = false,
+ /**
+ * List of tags in this room.
+ */
val tags: List = emptyList(),
+ /**
+ * Current user membership in this room.
+ */
val membership: Membership = Membership.NONE,
+ /**
+ * Versioning state of this room.
+ */
val versioningState: VersioningState = VersioningState.NONE,
+ /**
+ * Value of `m.fully_read` for this room.
+ */
val readMarkerId: String? = null,
+ /**
+ * Message saved as draft for this room.
+ */
val userDrafts: List = emptyList(),
+ /**
+ * True if this room is encrypted.
+ */
val isEncrypted: Boolean,
+ /**
+ * Timestamp of the `m.room.encryption` state event.
+ */
val encryptionEventTs: Long?,
+ /**
+ * List of users who are currently typing on this room.
+ */
val typingUsers: List,
+ /**
+ * UserId of the user who has invited the current user to this room.
+ */
val inviterId: String? = null,
+ /**
+ * Breadcrumb index, util to sort rooms by last seen.
+ */
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
+ /**
+ * The room encryption trust level.
+ * @see [RoomEncryptionTrustLevel]
+ */
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
+ /**
+ * True if a message has not been sent in this room.
+ */
val hasFailedSending: Boolean = false,
+ /**
+ * The type of the room. Null for regular room.
+ * @see [RoomType]
+ */
val roomType: String? = null,
+ /**
+ * List of parent spaces.
+ */
val spaceParents: List? = null,
+ /**
+ * List of children space.
+ */
val spaceChildren: List? = null,
+ /**
+ * List of all the space parents. Will be empty by default, you have to explicitly request it.
+ */
+ val flattenParents: List = emptyList(),
+ /**
+ * List of all the space parent Ids.
+ */
val flattenParentIds: List = emptyList(),
- val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null
+ /**
+ * Information about the encryption algorithm, if this room is encrypted.
+ */
+ val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null,
) {
-
+ /**
+ * True if [versioningState] is not [VersioningState.NONE].
+ */
val isVersioned: Boolean
get() = versioningState != VersioningState.NONE
+ /**
+ * True if [notificationCount] is not `0`.
+ */
val hasNewMessages: Boolean
get() = notificationCount != 0
+ /**
+ * True if the room has the tag `m.lowpriority`.
+ */
val isLowPriority: Boolean
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
+ /**
+ * True if the room has the tag `m.favourite`.
+ */
val isFavorite: Boolean
get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
+ /**
+ * True if [joinRules] is [RoomJoinRules.PUBLIC].
+ */
val isPublic: Boolean
get() = joinRules == RoomJoinRules.PUBLIC
+ /**
+ * Test if the room has the provided [tag].
+ */
fun hasTag(tag: String) = tags.any { it.name == tag }
+ /**
+ * True if a 1-1 call can be started, i.e. the room has exactly 2 joined members.
+ */
val canStartCall: Boolean
get() = joinedMembersCount == 2
companion object {
+ /**
+ * Constant to indicated that the room is not on the breadcrumbs.
+ * Used by [breadcrumbsIndex].
+ */
const val NOT_IN_BREADCRUMBS = -1
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
index b4e7b10d44..2e1668ebbb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
@@ -16,8 +16,22 @@
package org.matrix.android.sdk.api.session.room.model
+/**
+ * Enum for the versioning state of a room.
+ */
enum class VersioningState {
+ /**
+ * The room is not versioned.
+ */
NONE,
+
+ /**
+ * The room has been upgraded, but the new room is not joined yet.
+ */
UPGRADED_ROOM_NOT_JOINED,
- UPGRADED_ROOM_JOINED
+
+ /**
+ * The room has been upgraded, and the new room has been joined.
+ */
+ UPGRADED_ROOM_JOINED,
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
index 0b28d62f56..5ad1a48217 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt
@@ -22,6 +22,10 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati
* Aggregation info concerning a live location share.
*/
data class LiveLocationShareAggregatedSummary(
+ val userId: String?,
+ /**
+ * Indicate whether the live is currently running.
+ */
val isActive: Boolean?,
val endOfLiveTimestampMillis: Long?,
val lastLocationDataContent: MessageBeaconLocationDataContent?,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt
index 7c806bf35b..d058ff2840 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt
@@ -17,27 +17,44 @@
package org.matrix.android.sdk.api.session.room.send
enum class SendState {
+ /**
+ * The state is unknown.
+ */
UNKNOWN,
- // the event has not been sent
+ /**
+ * The event has not been sent.
+ */
UNSENT,
- // the event is encrypting
+ /**
+ * The event is encrypting.
+ */
ENCRYPTING,
- // the event is currently sending
+ /**
+ * The event is currently sending.
+ */
SENDING,
- // the event has been sent
+ /**
+ * The event has been sent.
+ */
SENT,
- // the event has been received from server
+ /**
+ * The event has been received from server.
+ */
SYNCED,
- // The event failed to be sent
+ /**
+ * The event failed to be sent.
+ */
UNDELIVERED,
- // the event failed to be sent because some unknown devices have been found while encrypting it
+ /**
+ * The event failed to be sent because some unknown devices have been found while encrypting it.
+ */
FAILED_UNKNOWN_DEVICES;
internal companion object {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt
new file mode 100644
index 0000000000..5a1bf67fdd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.securestorage
+
+data class KeyRef(
+ val keyId: String?,
+ val keySpec: SsssKeySpec?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt
index 057fb4c15e..bdbbd3ea84 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt
@@ -136,9 +136,4 @@ interface SharedSecretStorageService {
fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult
suspend fun requestSecret(name: String, myOtherDeviceId: String)
-
- data class KeyRef(
- val keyId: String?,
- val keySpec: SsssKeySpec?
- )
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
index c110802d23..6825f8c279 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
@@ -20,14 +20,19 @@ package org.matrix.android.sdk.api.session.threads
* This class defines the state of a thread notification.
*/
enum class ThreadNotificationState {
-
- // There are no new message
+ /**
+ * There are no new message.
+ */
NO_NEW_MESSAGE,
- // There is at least one new message
+ /**
+ * There is at least one new message.
+ */
NEW_MESSAGE,
- // The is at least one new message that should be highlighted
- // ex. "Hello @aris.kotsomitopoulos"
+ /**
+ * The is at least one new message that should be highlighted.
+ * ex. "Hello @aris.kotsomitopoulos"
+ */
NEW_HIGHLIGHTED_MESSAGE;
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 858a6448eb..61a423669c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -130,16 +130,7 @@ internal class DefaultAuthenticationService @Inject constructor(
?.trim { it == '/' }
}
- /**
- * This is the entry point of the authentication service.
- * homeServerConnectionConfig contains a homeserver URL probably entered by the user, which can be a
- * valid homeserver API url, the url of Element Web, or anything else.
- */
override suspend fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
- pendingSessionData = null
-
- pendingSessionStore.delete()
-
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
@@ -323,8 +314,7 @@ internal class DefaultAuthenticationService @Inject constructor(
}
}
- override val isRegistrationStarted: Boolean
- get() = currentRegistrationWizard?.isRegistrationStarted == true
+ override fun isRegistrationStarted() = currentRegistrationWizard?.isRegistrationStarted() == true
override fun getLoginWizard(): LoginWizard {
return currentLoginWizard
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
index a82cfc99bb..b45c4e87f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
@@ -49,20 +49,18 @@ internal class DefaultRegistrationWizard(
private val validateCodeTask: ValidateCodeTask = DefaultValidateCodeTask(authAPI)
private val registerCustomTask: RegisterCustomTask = DefaultRegisterCustomTask(authAPI)
- override val currentThreePid: String?
- get() {
- return when (val threePid = pendingSessionData.currentThreePidData?.threePid) {
- is RegisterThreePid.Email -> threePid.email
- is RegisterThreePid.Msisdn -> {
- // Take formatted msisdn if provided by the server
- pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn
- }
- null -> null
+ override fun getCurrentThreePid(): String? {
+ return when (val threePid = pendingSessionData.currentThreePidData?.threePid) {
+ is RegisterThreePid.Email -> threePid.email
+ is RegisterThreePid.Msisdn -> {
+ // Take formatted msisdn if provided by the server
+ pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn
}
+ null -> null
}
+ }
- override val isRegistrationStarted: Boolean
- get() = pendingSessionData.isRegistrationStarted
+ override fun isRegistrationStarted() = pendingSessionData.isRegistrationStarted
override suspend fun getRegistrationFlow(): RegistrationResult {
val params = RegistrationParams()
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 cb61bbe1de..c1d04eb22b 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
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
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.content.OlmEventContent
+import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@@ -42,7 +43,7 @@ import javax.inject.Inject
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
-private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
+private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope
internal class EventDecryptor @Inject constructor(
@@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor(
if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
+ } else if (event.isRedacted()) {
+ // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
+ return MXEventDecryptionResult(
+ clearEvent = mapOf(
+ "room_id" to event.roomId.orEmpty(),
+ "type" to EventType.MESSAGE,
+ "content" to emptyMap(),
+ "unsigned" to event.unsignedData.toContent()
+ )
+ )
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
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 8fe200a349..24b6fd166f 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
@@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor(
// So, store these message indexes per timeline id.
//
// The first level keys are timeline ids.
- // The second level keys are strings of form "||"
- private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap()
+ // The second level values is a Map that represents:
+ // "|||" --> eventId
+ private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap()
init {
// Retrieve the account from the store
@@ -757,15 +758,17 @@ internal class MXOlmDevice @Inject constructor(
* @param body the base64-encoded body of the encrypted message.
* @param roomId the room in which the message was received.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+ * @param eventId the eventId of the message that will be decrypted
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
- * @return the decrypting result. Nil if the sessionId is unknown.
+ * @return the decrypting result. Null if the sessionId is unknown.
*/
@Throws(MXCryptoError::class)
suspend fun decryptGroupMessage(
body: String,
roomId: String,
timeline: String?,
+ eventId: String,
sessionId: String,
senderKey: String
): OlmDecryptionResult {
@@ -773,53 +776,56 @@ internal class MXOlmDevice @Inject constructor(
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 == wrapper.roomId) {
- val decryptResult = try {
- sessionHolder.mutex.withLock {
- inboundGroupSession.decryptMessage(body)
- }
- } catch (e: OlmException) {
- Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
- throw MXCryptoError.OlmError(e)
- }
-
- if (timeline?.isNotBlank() == true) {
- val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
-
- val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
-
- if (timelineSet.contains(messageIndexKey)) {
- val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
- Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
- throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
- }
-
- timelineSet.add(messageIndexKey)
- }
-
- 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.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,
- wrapper.keysClaimed,
- senderKey,
- wrapper.forwardingCurve25519KeyChain
- )
- } else {
+ if (roomId != wrapper.roomId) {
+ // Check that the room id matches the original one for the session. This stops
+ // the HS pretending a message was targeting a different room.
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)
}
+ val decryptResult = try {
+ sessionHolder.mutex.withLock {
+ inboundGroupSession.decryptMessage(body)
+ }
+ } catch (e: OlmException) {
+ Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
+ throw MXCryptoError.OlmError(e)
+ }
+
+ val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex
+ Timber.tag(loggerTag.value).v("##########################################################")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId")
+ Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}")
+
+ if (timeline?.isNotBlank() == true) {
+ val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() }
+ if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) {
+ val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
+ }
+ replayAttackMap[messageIndexKey] = eventId
+ }
+ 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.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,
+ wrapper.keysClaimed,
+ senderKey,
+ wrapper.forwardingCurve25519KeyChain
+ )
}
/**
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 92cff5d9fa..141d6f74cd 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
@@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
encryptedEventContent.ciphertext,
event.roomId,
timeline,
+ eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)
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 c579930e1a..8ecb1d72c6 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
@@ -137,17 +137,11 @@ internal class DefaultKeysBackupService @Inject constructor(
private var keysBackupStateListener: KeysBackupStateListener? = null
- override val isEnabled: Boolean
- get() = keysBackupStateManager.isEnabled
+ override fun isEnabled(): Boolean = keysBackupStateManager.isEnabled
- override val isStucked: Boolean
- get() = keysBackupStateManager.isStucked
+ override fun isStuck(): Boolean = keysBackupStateManager.isStuck
- override val state: KeysBackupState
- get() = keysBackupStateManager.state
-
- override val currentBackupVersion: String?
- get() = keysBackupVersion?.version
+ override fun getState(): KeysBackupState = keysBackupStateManager.state
override fun addListener(listener: KeysBackupStateListener) {
keysBackupStateManager.addListener(listener)
@@ -295,7 +289,7 @@ internal class DefaultKeysBackupService @Inject constructor(
this.callback = object : MatrixCallback {
private fun eventuallyRestartBackup() {
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
- if (state == KeysBackupState.Unknown) {
+ if (getState() == KeysBackupState.Unknown) {
checkAndStartKeysBackup()
}
}
@@ -328,7 +322,7 @@ internal class DefaultKeysBackupService @Inject constructor(
// val hashServer = keysBackupData?.backupLastServerHash
return when {
- totalNumberOfKeysLocally < totalNumberOfKeysServer -> {
+ totalNumberOfKeysLocally < totalNumberOfKeysServer -> {
// Server contains more keys than this device
true
}
@@ -337,7 +331,7 @@ internal class DefaultKeysBackupService @Inject constructor(
// TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment
false
}
- else -> false
+ else -> false
}
}
@@ -353,7 +347,7 @@ internal class DefaultKeysBackupService @Inject constructor(
progressListener: ProgressListener?,
callback: MatrixCallback?
) {
- if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) {
+ if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) {
callback?.onFailure(Throwable("Backup not enabled"))
return
}
@@ -389,7 +383,7 @@ internal class DefaultKeysBackupService @Inject constructor(
}
// If backup is finished, notify the main listener
- if (state === KeysBackupState.ReadyToBackUp) {
+ if (getState() === KeysBackupState.ReadyToBackUp) {
backupAllGroupSessionsCallback?.onSuccess(Unit)
resetBackupAllGroupSessionsListeners()
}
@@ -642,7 +636,7 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
- override fun onSecretKeyGossip(secret: String) {
+ fun onSecretKeyGossip(secret: String) {
Timber.i("## CrossSigning - onSecretKeyGossip")
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
@@ -935,12 +929,12 @@ internal class DefaultKeysBackupService @Inject constructor(
*/
fun maybeBackupKeys() {
when {
- isStucked -> {
+ isStuck() -> {
// If not already done, or in error case, check for a valid backup version on the homeserver.
// If there is one, maybeBackupKeys will be called again.
checkAndStartKeysBackup()
}
- state == KeysBackupState.ReadyToBackUp -> {
+ getState() == KeysBackupState.ReadyToBackUp -> {
keysBackupStateManager.state = KeysBackupState.WillBackUp
// Wait between 0 and 10 seconds, to avoid backup requests from
@@ -953,8 +947,8 @@ internal class DefaultKeysBackupService @Inject constructor(
uiHandler.post { backupKeys() }
}
}
- else -> {
- Timber.v("maybeBackupKeys: Skip it because state: $state")
+ else -> {
+ Timber.v("maybeBackupKeys: Skip it because state: ${getState()}")
}
}
}
@@ -998,7 +992,7 @@ internal class DefaultKeysBackupService @Inject constructor(
override fun onSuccess(data: KeysBackupLastVersionResult) {
val localBackupVersion = keysBackupVersion?.version
when (data) {
- KeysBackupLastVersionResult.NoKeysBackup -> {
+ KeysBackupLastVersionResult.NoKeysBackup -> {
if (localBackupVersion == null) {
// No backup on the server, and backup is not active
callback.onSuccess(true)
@@ -1040,9 +1034,9 @@ internal class DefaultKeysBackupService @Inject constructor(
}
override fun checkAndStartKeysBackup() {
- if (!isStucked) {
+ if (!isStuck()) {
// Try to start or restart the backup only if it is in unknown or bad state
- Timber.w("checkAndStartKeysBackup: invalid state: $state")
+ Timber.w("checkAndStartKeysBackup: invalid state: ${getState()}")
return
}
@@ -1283,16 +1277,16 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.v("backupKeys")
// Sanity check, as this method can be called after a delay, the state may have change during the delay
- if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) {
+ if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) {
Timber.v("backupKeys: Invalid configuration")
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
resetBackupAllGroupSessionsListeners()
return
}
- if (state === KeysBackupState.BackingUp) {
+ if (getState() === KeysBackupState.BackingUp) {
// Do nothing if we are already backing up
- Timber.v("backupKeys: Invalid state: $state")
+ Timber.v("backupKeys: Invalid state: ${getState()}")
return
}
@@ -1384,7 +1378,7 @@ internal class DefaultKeysBackupService @Inject constructor(
// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
- else ->
+ else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt
index 78ef958bbf..0614eceb16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt
@@ -49,7 +49,7 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) {
state == KeysBackupState.BackingUp
// True if unknown or bad state
- val isStucked: Boolean
+ val isStuck: Boolean
get() = state == KeysBackupState.Unknown ||
state == KeysBackupState.Disabled ||
state == KeysBackupState.WrongBackUpVersion ||
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
index a71af991a7..ddb048a912 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfo
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.KeySigner
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent
@@ -161,7 +162,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return getKey(keyId)
}
- override suspend fun storeSecret(name: String, secretBase64: String, keys: List) {
+ override suspend fun storeSecret(name: String, secretBase64: String, keys: List) {
withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.computation) {
val encryptedContents = HashMap()
keys.forEach {
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 72014ff3bb..b18de34329 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
@@ -483,8 +483,12 @@ internal interface IMXCryptoStore {
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
fun markedSessionAsShared(
- roomId: String?, sessionId: String, userId: String, deviceId: String,
- deviceIdentityKey: String, chainIndex: Int
+ roomId: String?,
+ sessionId: String,
+ userId: String,
+ deviceId: String,
+ deviceIdentityKey: String,
+ chainIndex: Int
)
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
index 97fab9174d..85dd50c88e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
@@ -51,7 +51,8 @@ internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, se
}
internal fun SharedSessionEntity.Companion.create(
- realm: Realm, roomId: String?,
+ realm: Realm,
+ roomId: String?,
sessionId: String,
userId: String,
deviceId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
index b683c8905f..9d19fd137e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
@@ -59,7 +59,8 @@ internal abstract class DefaultVerificationTransaction(
protected fun trust(
canTrustOtherUserMasterKey: Boolean,
toVerifyDeviceIds: List,
- eventuallyMarkMyMasterKeyAsTrusted: Boolean, autoDone: Boolean = true
+ eventuallyMarkMyMasterKeyAsTrusted: Boolean,
+ autoDone: Boolean = true
) {
Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds")
Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
index 350c5e8cd1..f38a604890 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
@@ -164,8 +164,12 @@ internal class VerificationTransportRoomMessage(
}
}
- override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) =
- cancelTransaction(transactionId, otherUserId, null, code)
+ override fun cancelTransaction(
+ transactionId: String,
+ otherUserId: String,
+ otherUserDeviceIds: List,
+ code: CancelCode
+ ) = cancelTransaction(transactionId, otherUserId, null, code)
override fun done(
transactionId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 55bccfd1ec..592461f927 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber
import javax.inject.Inject
@@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000
- val schemaVersion = 28L
+ val schemaVersion = 29L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -93,5 +94,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 26) MigrateSessionTo026(realm).perform()
if (oldVersion < 27) MigrateSessionTo027(realm).perform()
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
+ if (oldVersion < 29) MigrateSessionTo029(realm).perform()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 5b2e27c24a..dfac7f6708 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -47,7 +47,8 @@ private typealias Summary = Pair?
*/
internal fun Map.updateThreadSummaryIfNeeded(
roomId: String,
- realm: Realm, currentUserId: String,
+ realm: Realm,
+ currentUserId: String,
chunkEntity: ChunkEntity? = null,
shouldUpdateNotifications: Boolean = true
) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
index c747ad334f..6bbeb17fdd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
@@ -58,7 +58,7 @@ internal object EventAnnotationsSummaryMapper {
PollResponseAggregatedSummaryEntityMapper.map(it)
},
liveLocationShareAggregatedSummary = annotationsSummary.liveLocationShareAggregatedSummary?.let {
- LiveLocationShareAggregatedSummaryMapper.map(it)
+ LiveLocationShareAggregatedSummaryMapper().map(it)
}
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
index 71b36f88bd..9460e4c6ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
@@ -20,11 +20,13 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import javax.inject.Inject
-internal object LiveLocationShareAggregatedSummaryMapper {
+internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() {
fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
return LiveLocationShareAggregatedSummary(
+ userId = entity.userId,
isActive = entity.isActive,
endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis,
lastLocationDataContent = ContentMapper.map(entity.lastLocationContent).toModel()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt
new file mode 100644
index 0000000000..aebca11c2b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live location sharing aggregated summary: adding new field userId.
+ */
+internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
+ ?.addField(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, String::class.java, FieldAttribute.REQUIRED)
+ ?.transform { obj ->
+ obj.setString(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, "")
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
index 4d0d2c5c64..c5df8e9338 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
@@ -31,6 +31,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
var roomId: String = "",
+ var userId: String = "",
+
+ /**
+ * Indicate whether the live is currently running.
+ */
var isActive: Boolean? = null,
var endOfLiveTimestampMillis: Long? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
index 2e2e939fa2..0cc41413f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
@@ -28,9 +28,15 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
roomId: String,
eventId: String,
): RealmQuery {
+ return LiveLocationShareAggregatedSummaryEntity
+ .whereRoomId(realm, roomId = roomId)
+ .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
+}
+
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm,
+ roomId: String): RealmQuery {
return realm.where()
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
- .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
}
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create(
@@ -55,3 +61,39 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.getOrCreate(
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
?: LiveLocationShareAggregatedSummaryEntity.create(realm, roomId, eventId)
}
+
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
+ realm: Realm,
+ roomId: String,
+ eventId: String,
+): LiveLocationShareAggregatedSummaryEntity? {
+ return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
+}
+
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
+ realm: Realm,
+ roomId: String,
+ userId: String,
+ ignoredEventId: String
+): List {
+ return LiveLocationShareAggregatedSummaryEntity
+ .whereRoomId(realm, roomId = roomId)
+ .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId)
+ .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+ .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
+ .findAll()
+}
+
+/**
+ * A live is considered as running when active and with at least a last known location.
+ */
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveInRoom(
+ realm: Realm,
+ roomId: String,
+): RealmQuery {
+ return LiveLocationShareAggregatedSummaryEntity
+ .whereRoomId(realm, roomId = roomId)
+ .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+ .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
+ .isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index f53a89130a..f01451b688 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule
import org.matrix.android.sdk.internal.session.room.RoomModule
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@@ -131,6 +132,8 @@ internal interface SessionComponent {
fun inject(worker: UpdateTrustWorker)
+ fun inject(worker: DeactivateLiveLocationShareWorker)
+
@Component.Factory
interface Factory {
fun create(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt
index 71e9743c6a..5e892b32c3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt
@@ -36,8 +36,7 @@ internal abstract class BindThreePidsTask : Task
internal class DefaultBindThreePidsTask @Inject constructor(
private val profileAPI: ProfileAPI,
private val identityStore: IdentityStore,
- @AuthenticatedIdentity
- private val accessTokenProvider: AccessTokenProvider,
+ @AuthenticatedIdentity private val accessTokenProvider: AccessTokenProvider,
private val globalErrorReceiver: GlobalErrorReceiver
) : BindThreePidsTask() {
override suspend fun execute(params: Params) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
index 892ada38bd..e912d9ccf8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.pushers
import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.pushers.HttpPusher
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.pushers.PushersService
import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -60,15 +61,15 @@ internal class DefaultPushersService @Inject constructor(
.executeBy(taskExecutor)
}
- override fun enqueueAddHttpPusher(httpPusher: PushersService.HttpPusher): UUID {
+ override fun enqueueAddHttpPusher(httpPusher: HttpPusher): UUID {
return enqueueAddPusher(httpPusher.toJsonPusher())
}
- override suspend fun addHttpPusher(httpPusher: PushersService.HttpPusher) {
+ override suspend fun addHttpPusher(httpPusher: HttpPusher) {
addPusherTask.execute(AddPusherTask.Params(httpPusher.toJsonPusher()))
}
- private fun PushersService.HttpPusher.toJsonPusher() = JsonPusher(
+ private fun HttpPusher.toJsonPusher() = JsonPusher(
pushKey = pushkey,
kind = "http",
appId = appId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 7326adee4c..abea2d34cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic
import org.matrix.android.sdk.api.session.room.alias.AliasService
import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
+import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
@@ -69,6 +70,7 @@ internal class DefaultRoom(
private val roomAccountDataService: RoomAccountDataService,
private val roomVersionService: RoomVersionService,
private val viaParameterFinder: ViaParameterFinder,
+ private val locationSharingService: LocationSharingService,
override val coroutineDispatchers: MatrixCoroutineDispatchers
) : Room {
@@ -104,4 +106,5 @@ internal class DefaultRoom(
override fun roomPushRuleService() = roomPushRuleService
override fun roomAccountDataService() = roomAccountDataService
override fun roomVersionService() = roomVersionService
+ override fun locationSharingService() = locationSharingService
}
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 b51f3d73f4..7a7804c354 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,7 +20,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.api.session.room.Room
@@ -91,6 +90,10 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getRoomSummary(roomIdOrAlias)
}
+ override fun getRoomSummaryLive(roomId: String): LiveData> {
+ return roomSummaryDataSource.getRoomSummaryLive(roomId)
+ }
+
override fun getRoomSummaries(
queryParams: RoomSummaryQueryParams,
sortOrder: RoomSortOrder
@@ -99,12 +102,15 @@ internal class DefaultRoomService @Inject constructor(
}
override fun refreshJoinedRoomSummaryPreviews(roomId: String?) {
- val roomSummaries = getRoomSummaries(roomSummaryQueryParams {
- if (roomId != null) {
- this.roomId = QueryStringValue.Equals(roomId)
- }
- memberships = listOf(Membership.JOIN)
- })
+ val roomSummaries = if (roomId == null) {
+ getRoomSummaries(roomSummaryQueryParams {
+ memberships = listOf(Membership.JOIN)
+ })
+ } else {
+ listOfNotNull(
+ getRoomSummary(roomId)?.takeIf { it.membership == Membership.JOIN }
+ )
+ }
if (roomSummaries.isNotEmpty()) {
monarchy.runTransactionSync { realm ->
@@ -135,7 +141,7 @@ internal class DefaultRoomService @Inject constructor(
pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder
): UpdatableLivePageResult {
- return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
+ return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenedParents = true)
}
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 01c4fd1501..ffe7679575 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService
import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService
import org.matrix.android.sdk.internal.session.room.crypto.DefaultRoomCryptoService
import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService
+import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService
import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService
import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService
import org.matrix.android.sdk.internal.session.room.read.DefaultReadService
@@ -69,6 +70,7 @@ internal class DefaultRoomFactory @Inject constructor(
private val roomVersionServiceFactory: DefaultRoomVersionService.Factory,
private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory,
private val viaParameterFinder: ViaParameterFinder,
+ private val locationSharingServiceFactory: DefaultLocationSharingService.Factory,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : RoomFactory {
@@ -96,6 +98,7 @@ internal class DefaultRoomFactory @Inject constructor(
roomAccountDataService = roomAccountDataServiceFactory.create(roomId),
roomVersionService = roomVersionServiceFactory.create(roomId),
viaParameterFinder = viaParameterFinder,
+ locationSharingService = locationSharingServiceFactory.create(roomId),
coroutineDispatchers = coroutineDispatchers
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt
new file mode 100644
index 0000000000..2b83c8028b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import com.squareup.moshi.JsonClass
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.api.util.md5
+import org.matrix.android.sdk.internal.SessionManager
+import org.matrix.android.sdk.internal.database.awaitTransaction
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.get
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.SessionComponent
+import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
+import org.matrix.android.sdk.internal.worker.SessionWorkerParams
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Worker dedicated to update live location summary data so that it is considered as deactivated.
+ * For the context: it is needed since a live location share should be deactivated after a certain timeout.
+ */
+internal class DeactivateLiveLocationShareWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
+ SessionSafeCoroutineWorker(
+ context,
+ params,
+ sessionManager,
+ Params::class.java
+ ) {
+
+ @JsonClass(generateAdapter = true)
+ internal data class Params(
+ override val sessionId: String,
+ override val lastFailureMessage: String? = null,
+ val eventId: String,
+ val roomId: String
+ ) : SessionWorkerParams
+
+ @SessionDatabase
+ @Inject lateinit var realmConfiguration: RealmConfiguration
+
+ override fun injectWith(injector: SessionComponent) {
+ injector.inject(this)
+ }
+
+ override suspend fun doSafeWork(params: Params): Result {
+ return runCatching {
+ deactivateLiveLocationShare(params)
+ }.fold(
+ onSuccess = {
+ Result.success()
+ },
+ onFailure = {
+ Timber.e("failed to deactivate live, eventId: ${params.eventId}, roomId: ${params.roomId}")
+ Result.failure()
+ }
+ )
+ }
+
+ private suspend fun deactivateLiveLocationShare(params: Params) {
+ awaitTransaction(realmConfiguration) { realm ->
+ val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.get(
+ realm = realm,
+ roomId = params.roomId,
+ eventId = params.eventId
+ )
+ aggregatedSummary?.isActive = false
+ }
+ }
+
+ override fun buildErrorParams(params: Params, message: String): Params {
+ return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
+ }
+
+ companion object {
+ fun getWorkName(eventId: String, roomId: String): String {
+ val hash = "$eventId$roomId".md5()
+ return "DeactivateLiveLocationWork-$hash"
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
index 76b7a4ec8e..8f4682a9d5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
+import androidx.work.ExistingWorkPolicy
import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.events.model.Event
@@ -25,18 +26,30 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findActiveLiveInRoomForUser
import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.di.SessionId
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
+import org.matrix.android.sdk.internal.util.time.Clock
+import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
-internal class LiveLocationAggregationProcessor @Inject constructor() {
+// TODO add unit tests
+internal class LiveLocationAggregationProcessor @Inject constructor(
+ @SessionId private val sessionId: String,
+ private val workManagerProvider: WorkManagerProvider,
+ private val clock: Clock,
+) {
fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
return
}
- val targetEventId = if (content.isLive.orTrue()) {
+ val isLive = content.isLive.orTrue()
+ val targetEventId = if (isLive) {
event.eventId
} else {
// when live is set to false, we use the id of the event that should have been replaced
@@ -56,8 +69,42 @@ internal class LiveLocationAggregationProcessor @Inject constructor() {
Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}")
- aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
- aggregatedSummary.isActive = content.isLive
+ val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
+ aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
+ aggregatedSummary.isActive = isLive
+ aggregatedSummary.userId = event.senderId
+
+ deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId)
+
+ if (isLive) {
+ scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
+ } else {
+ cancelDeactivationAfterTimeout(targetEventId, roomId)
+ }
+ }
+
+ private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) {
+ endOfLiveTimestampMillis ?: return
+
+ val workParams = DeactivateLiveLocationShareWorker.Params(sessionId = sessionId, eventId = eventId, roomId = roomId)
+ val workData = WorkerParamsFactory.toData(workParams)
+ val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId)
+ val workDelayMillis = (endOfLiveTimestampMillis - clock.epochMillis()).coerceAtLeast(0)
+ val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder()
+ .setInitialDelay(workDelayMillis, TimeUnit.MILLISECONDS)
+ .setInputData(workData)
+ .build()
+
+ workManagerProvider.workManager.enqueueUniqueWork(
+ workName,
+ ExistingWorkPolicy.REPLACE,
+ workRequest
+ )
+ }
+
+ private fun cancelDeactivationAfterTimeout(eventId: String, roomId: String) {
+ val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId)
+ workManagerProvider.workManager.cancelUniqueWork(workName)
}
fun handleBeaconLocationData(
@@ -95,5 +142,16 @@ internal class LiveLocationAggregationProcessor @Inject constructor() {
}
}
+ private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) {
+ LiveLocationShareAggregatedSummaryEntity
+ .findActiveLiveInRoomForUser(
+ realm = realm,
+ roomId = roomId,
+ userId = userId,
+ ignoredEventId = currentEventId
+ )
+ .forEach { it.isActive = false }
+ }
+
private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
new file mode 100644
index 0000000000..8cf6fcdfbf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.location
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import org.matrix.android.sdk.api.session.room.location.LocationSharingService
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom
+import org.matrix.android.sdk.internal.di.SessionDatabase
+
+// TODO add unit tests
+internal class DefaultLocationSharingService @AssistedInject constructor(
+ @Assisted private val roomId: String,
+ @SessionDatabase private val monarchy: Monarchy,
+ private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
+) : LocationSharingService {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(roomId: String): DefaultLocationSharingService
+ }
+
+ override fun getRunningLiveLocationShareSummaries(): LiveData> {
+ return monarchy.findAllMappedWithChanges(
+ { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
+ { liveLocationShareAggregatedSummaryMapper.map(it) }
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt
index 5bad334afc..fffca96acf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt
@@ -43,7 +43,6 @@ internal class RoomChildRelationInfo(
data class SpaceChildInfo(
val roomId: String,
val order: String?,
-// val autoJoin: Boolean,
val viaServers: List
)
@@ -60,18 +59,13 @@ internal class RoomChildRelationInfo(
fun getDirectChildrenDescriptions(): List {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
.findAll()
-// .also {
-// Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId")
-// }
.mapNotNull {
ContentMapper.map(it.root?.content).toModel()?.let { scc ->
-// Timber.v("## Space child desc state event $scc")
// Children where via is not present are ignored.
scc.via?.let { via ->
SpaceChildInfo(
roomId = it.stateKey,
order = scc.validOrder(),
-// autoJoin = scc.autoJoin ?: false,
viaServers = via
)
}
@@ -83,17 +77,13 @@ internal class RoomChildRelationInfo(
fun getParentDescriptions(): List {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT)
.findAll()
-// .also {
-// Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId")
-// }
.mapNotNull {
- ContentMapper.map(it.root?.content).toModel()?.let { scc ->
-// Timber.v("## Space parent desc state event $scc")
+ ContentMapper.map(it.root?.content).toModel()?.let { spaceParentContent ->
// Parent where via is not present are ignored.
- scc.via?.let { via ->
+ spaceParentContent.via?.let { via ->
SpaceParentInfo(
roomId = it.stateKey,
- canonical = scc.canonical ?: false,
+ canonical = spaceParentContent.canonical ?: false,
viaServers = via,
stateEventSender = it.root?.sender ?: ""
)
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 b71156a431..b3467c694d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -26,8 +26,9 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
-import org.matrix.android.sdk.api.query.ActiveSpaceFilter
+import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
+import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.query.isNormalized
import org.matrix.android.sdk.api.session.room.ResultBoundaries
import org.matrix.android.sdk.api.session.room.RoomSortOrder
@@ -198,14 +199,15 @@ internal class RoomSummaryDataSource @Inject constructor(
fun getUpdatablePagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config,
- sortOrder: RoomSortOrder
+ sortOrder: RoomSortOrder,
+ getFlattenedParents: Boolean = false
): UpdatableLivePageResult {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams).process(sortOrder)
}
val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it)
- }
+ }.map { if (getFlattenedParents) it.getWithParents() else it }
val boundaries = MutableLiveData(ResultBoundaries())
@@ -244,6 +246,13 @@ internal class RoomSummaryDataSource @Inject constructor(
}
}
+ private fun RoomSummary.getWithParents(): RoomSummary {
+ val parents = flattenParentIds.mapNotNull { parentId ->
+ getRoomSummary(parentId)
+ }
+ return copy(flattenParents = parents)
+ }
+
fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData {
val liveRooms = monarchy.findAllManagedWithChanges {
roomSummariesQuery(it, queryParams)
@@ -270,29 +279,13 @@ internal class RoomSummaryDataSource @Inject constructor(
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery {
val query = with(queryStringValueProcessor) {
RoomSummaryEntity.where(realm)
- .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
- .let {
- if (queryParams.displayName.isNormalized()) {
- it.process(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, queryParams.displayName)
- } else {
- it.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
- }
- }
+ .process(RoomSummaryEntityFields.ROOM_ID, QueryStringValue.IsNotEmpty)
+ .process(queryParams.displayName.toDisplayNameField(), queryParams.displayName)
.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
.equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false)
}
- queryParams.roomCategoryFilter?.let {
- when (it) {
- RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
- RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
- RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
- RoomCategoryFilter.ALL -> {
- // nop
- }
- }
- }
queryParams.roomTagQueryFilter?.let {
it.isFavorite?.let { fav ->
query.equalTo(RoomSummaryEntityFields.IS_FAVOURITE, fav)
@@ -312,31 +305,27 @@ internal class RoomSummaryDataSource @Inject constructor(
query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it)
}
when (queryParams.roomCategoryFilter) {
- RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
- RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+ RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+ RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
- RoomCategoryFilter.ALL -> Unit // nop
- null -> Unit
+ null -> Unit
}
// Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
- when (queryParams.activeSpaceFilter) {
- is ActiveSpaceFilter.ActiveSpace -> {
+ when (queryParams.spaceFilter) {
+ SpaceFilter.OrphanRooms -> {
+ // orphan rooms
+ query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
+ }
+ is SpaceFilter.ActiveSpace -> {
// It's annoying but for now realm java does not support querying in primitive list :/
// https://github.com/realm/realm-java/issues/5361
- if (queryParams.activeSpaceFilter.currentSpaceId == null) {
- // orphan rooms
- query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
- } else {
- query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.currentSpaceId)
- }
+ query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId)
}
- is ActiveSpaceFilter.ExcludeSpace -> {
- query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.spaceId)
- }
- else -> {
- // nop
+ is SpaceFilter.ExcludeSpace -> {
+ query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId)
}
+ null -> Unit // nop
}
queryParams.activeGroupId?.let { activeGroupId ->
@@ -345,6 +334,14 @@ internal class RoomSummaryDataSource @Inject constructor(
return query
}
+ private fun QueryStringValue.toDisplayNameField(): String {
+ return if (isNormalized()) {
+ RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME
+ } else {
+ RoomSummaryEntityFields.DISPLAY_NAME
+ }
+ }
+
fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List): List {
val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList()
val result = ArrayList()
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 1d31e28c07..c79b61c8c7 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
@@ -486,6 +486,31 @@ internal class TimelineChunk(
private fun handleDatabaseChangeSet(results: RealmResults, changeSet: OrderedCollectionChangeSet) {
val insertions = changeSet.insertionRanges
for (range in insertions) {
+ // Check if the insertion's displayIndices match our expectations - or skip this insertion.
+ // Inconsistencies (missing messages) can happen otherwise if we get insertions before having loaded all timeline events of the chunk.
+ if (builtEvents.isNotEmpty()) {
+ // Check consistency to item before insertions
+ if (range.startIndex > 0) {
+ val firstInsertion = results[range.startIndex]!!
+ val lastBeforeInsertion = builtEvents[range.startIndex - 1]
+ if (firstInsertion.displayIndex + 1 != lastBeforeInsertion.displayIndex) {
+ Timber.i("handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
+ "displayIndex mismatch at ${range.startIndex}: ${firstInsertion.displayIndex} -> ${lastBeforeInsertion.displayIndex}")
+ continue
+ }
+ }
+ // Check consistency to item after insertions
+ if (range.startIndex < builtEvents.size) {
+ val lastInsertion = results[range.startIndex + range.length - 1]!!
+ val firstAfterInsertion = builtEvents[range.startIndex]
+ if (firstAfterInsertion.displayIndex + 1 != lastInsertion.displayIndex) {
+ Timber.i("handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
+ "displayIndex mismatch at ${range.startIndex + range.length}: " +
+ "${firstAfterInsertion.displayIndex} -> ${lastInsertion.displayIndex}")
+ continue
+ }
+ }
+ }
val newItems = results
.subList(range.startIndex, range.startIndex + range.length)
.map { it.buildAndDecryptIfNeeded() }
@@ -503,8 +528,12 @@ internal class TimelineChunk(
for (range in modifications) {
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
val updatedEntity = results[modificationIndex] ?: continue
+ val displayIndex = builtEventsIndexes[updatedEntity.eventId]
+ if (displayIndex == null) {
+ continue
+ }
try {
- builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded()
+ builtEvents[displayIndex] = updatedEntity.buildAndDecryptIfNeeded()
} catch (failure: Throwable) {
Timber.v("Fail to update items at index: $modificationIndex")
}
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 8fef229f97..e2794900f8 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
@@ -536,9 +536,10 @@ internal class RoomSyncHandler @Inject constructor(
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
+ val timelineId = generateTimelineId(roomId)
// Event from sync does not have roomId, so add it to the event first
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
- val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
+ val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) }
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@@ -553,6 +554,10 @@ internal class RoomSyncHandler @Inject constructor(
}
}
+ private fun generateTimelineId(roomId: String): String {
+ return "RoomSyncHandler$roomId"
+ }
+
data class EphemeralResult(
val typingUserIds: List = emptyList()
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
index 01c8a595d3..9e464a80dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@@ -48,25 +49,27 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
): ListenableWorker? {
Timber.d("MatrixWorkerFactory.createWorker for $workerClassName")
return when (workerClassName) {
- CheckFactoryWorker::class.java.name ->
+ CheckFactoryWorker::class.java.name ->
CheckFactoryWorker(appContext, workerParameters, true)
- AddPusherWorker::class.java.name ->
+ AddPusherWorker::class.java.name ->
AddPusherWorker(appContext, workerParameters, sessionManager)
- GetGroupDataWorker::class.java.name ->
+ GetGroupDataWorker::class.java.name ->
GetGroupDataWorker(appContext, workerParameters, sessionManager)
MultipleEventSendingDispatcherWorker::class.java.name ->
MultipleEventSendingDispatcherWorker(appContext, workerParameters, sessionManager)
- RedactEventWorker::class.java.name ->
+ RedactEventWorker::class.java.name ->
RedactEventWorker(appContext, workerParameters, sessionManager)
- SendEventWorker::class.java.name ->
+ SendEventWorker::class.java.name ->
SendEventWorker(appContext, workerParameters, sessionManager)
- SyncWorker::class.java.name ->
+ SyncWorker::class.java.name ->
SyncWorker(appContext, workerParameters, sessionManager)
- UpdateTrustWorker::class.java.name ->
+ UpdateTrustWorker::class.java.name ->
UpdateTrustWorker(appContext, workerParameters, sessionManager)
- UploadContentWorker::class.java.name ->
+ UploadContentWorker::class.java.name ->
UploadContentWorker(appContext, workerParameters, sessionManager)
- else -> {
+ DeactivateLiveLocationShareWorker::class.java.name ->
+ DeactivateLiveLocationShareWorker(appContext, workerParameters, sessionManager)
+ else -> {
Timber.w("No worker defined on MatrixWorkerFactory for $workerClassName will delegate to default.")
// Return null to delegate to the default WorkerFactory.
null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt
new file mode 100644
index 0000000000..06c6cc9ef9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Init a Matrix object.
+ *
+ * ### Required APIs:
+ * - [org.matrix.android.sdk.api.Matrix] constructor
+ */
+class Us000InitMatrix private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt
new file mode 100644
index 0000000000..f508a75db7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Sign in to an existing account.
+ *
+ * #### Required APIs:
+ * - [org.matrix.android.sdk.api.Matrix.authenticationService]
+ * - [org.matrix.android.sdk.api.auth.AuthenticationService.getLoginFlow]
+ * - [org.matrix.android.sdk.api.auth.AuthenticationService.getLoginWizard]
+ * - [org.matrix.android.sdk.api.auth.login.LoginWizard.login]
+ */
+class Us100SignIn private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt
new file mode 100644
index 0000000000..ac56cc35dd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Verify a Session after a Sign in.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us150VerifySession private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt
new file mode 100644
index 0000000000..0c74c0dfef
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Sign out.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us190SignOut private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt
new file mode 100644
index 0000000000..1f0b2e4103
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Get the Room list.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us200RoomList private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt
new file mode 100644
index 0000000000..5db06f95f6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Display a Room timeline, and navigate backward and forward.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us300RoomTimeline private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt
new file mode 100644
index 0000000000..5e0f6d3b3d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Display a Room timeline at a specific point.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us350RoomTimelineFromPermalink private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt
new file mode 100644
index 0000000000..3f72cd0b9e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Send content to a room, including monitoring the sending state.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us400RoomSendContent private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt
new file mode 100644
index 0000000000..c5c2107fa6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Get notified when new Events are received.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us500Notification private constructor()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt
new file mode 100644
index 0000000000..6b8fb87df7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+package org.matrix.android.sdk.userstories
+
+/**
+ * ### Title
+ * Manage the sync with the server.
+ *
+ * #### Required APIs:
+ * - TODO
+ */
+class Us600SyncWithTheServer private constructor()
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt
new file mode 100644
index 0000000000..47d5f46525
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.mapper
+
+import com.squareup.moshi.Moshi
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+
+private const val ANY_USER_ID = "a-user-id"
+private const val ANY_ACTIVE_STATE = true
+private const val ANY_TIMEOUT = 123L
+private val A_LOCATION_INFO = LocationInfo("a-geo-uri")
+
+class LiveLocationShareAggregatedSummaryMapperTest {
+
+ private val mapper = LiveLocationShareAggregatedSummaryMapper()
+
+ @Test
+ fun `given an entity then result should be mapped correctly`() {
+ val entity = anEntity(content = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO))
+
+ val summary = mapper.map(entity)
+
+ summary shouldBeEqualTo LiveLocationShareAggregatedSummary(
+ userId = ANY_USER_ID,
+ isActive = ANY_ACTIVE_STATE,
+ endOfLiveTimestampMillis = ANY_TIMEOUT,
+ lastLocationDataContent = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO)
+ )
+ }
+
+ private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity(
+ userId = ANY_USER_ID,
+ isActive = ANY_ACTIVE_STATE,
+ endOfLiveTimestampMillis = ANY_TIMEOUT,
+ lastLocationContent = Moshi.Builder().build().adapter(MessageBeaconLocationDataContent::class.java).toJson(content)
+ )
+}
diff --git a/vector-config/build.gradle b/vector-config/build.gradle
index 4156e74274..95b6a6215d 100644
--- a/vector-config/build.gradle
+++ b/vector-config/build.gradle
@@ -1,6 +1,5 @@
plugins {
id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
}
android {
@@ -14,7 +13,4 @@ android {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
- kotlinOptions {
- jvmTarget = "11"
- }
}
diff --git a/vector/build.gradle b/vector/build.gradle
index 4ae6527e0b..dea95b607a 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -31,7 +31,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 = 18
+ext.versionPatch = 20
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -370,7 +370,6 @@ dependencies {
implementation libs.androidx.lifecycleProcess
implementation libs.androidx.lifecycleRuntimeKtx
- implementation libs.androidx.datastore
implementation libs.androidx.datastorepreferences
@@ -381,12 +380,11 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
// FlowBinding
implementation libs.github.flowBinding
implementation libs.github.flowBindingAppcompat
- implementation libs.github.flowBindingMaterial
implementation libs.airbnb.epoxy
implementation libs.airbnb.epoxyGlide
@@ -507,9 +505,14 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// MapTiler
- implementation 'org.maplibre.gl:android-sdk:9.5.2'
- implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0'
-
+ fdroidImplementation(libs.maplibre.androidSdk) {
+ exclude group: 'com.google.android.gms', module: 'play-services-location'
+ }
+ fdroidImplementation(libs.maplibre.pluginAnnotation) {
+ exclude group: 'com.google.android.gms', module: 'play-services-location'
+ }
+ gplayImplementation libs.maplibre.androidSdk
+ gplayImplementation libs.maplibre.pluginAnnotation
// TESTS
testImplementation libs.tests.junit
diff --git a/vector/sampledata/live_location_users.json b/vector/sampledata/live_location_users.json
new file mode 100644
index 0000000000..58d0fb5fa1
--- /dev/null
+++ b/vector/sampledata/live_location_users.json
@@ -0,0 +1,14 @@
+{
+ "data": [
+ {
+ "displayName": "Amandine",
+ "remainingTime": "9min left",
+ "lastUpdatedAt": "Updated 12min ago"
+ },
+ {
+ "displayName": "You",
+ "remainingTime": "19min left",
+ "lastUpdatedAt": "Updated 1min ago"
+ }
+ ]
+}
diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
index 21fd226317..eaf39310a8 100644
--- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
@@ -165,7 +165,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
assert(uiSession.cryptoService().crossSigningService().isCrossSigningInitialized())
assert(uiSession.cryptoService().crossSigningService().canCrossSign())
assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown())
- assert(uiSession.cryptoService().keysBackupService().isEnabled)
+ assert(uiSession.cryptoService().keysBackupService().isEnabled())
assert(uiSession.cryptoService().keysBackupService().currentBackupVersion != null)
assert(uiSession.sharedSecretStorageService().isRecoverySetup())
assert(uiSession.sharedSecretStorageService().isMegolmKeyInBackup())
diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml
index 87aade0c8b..0b2b5cf90f 100644
--- a/vector/src/debug/AndroidManifest.xml
+++ b/vector/src/debug/AndroidManifest.xml
@@ -2,8 +2,6 @@
-
-
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
index 00a073f832..aa4df5e308 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
@@ -60,6 +60,11 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.onboardingCombinedRegister,
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
),
+ createBooleanFeature(
+ label = "FTUE Combined login",
+ key = DebugFeatureKeys.onboardingCombinedLogin,
+ factory = VectorFeatures::isOnboardingCombinedLoginEnabled
+ ),
)
)
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
index 1bc37ff97e..f36b1a804a 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -57,6 +57,9 @@ class DebugVectorFeatures(
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
+ override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin)
+ ?: vectorFeatures.isOnboardingCombinedLoginEnabled()
+
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
?: vectorFeatures.isScreenSharingEnabled()
@@ -113,6 +116,7 @@ object DebugFeatureKeys {
val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
+ val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login")
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
val screenSharing = booleanPreferencesKey("screen-sharing")
}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 20b7c4908a..2e31439e73 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -45,8 +45,6 @@
-
-
@@ -306,7 +304,9 @@
android:supportsPictureInPicture="true" />
-
+
+
@@ -343,6 +343,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt
index 46c94f3604..f25bcac92a 100644
--- a/vector/src/main/java/im/vector/app/AppStateHandler.kt
+++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt
@@ -72,6 +72,8 @@ class AppStateHandler @Inject constructor(
val selectedRoomGroupingFlow = selectedSpaceDataSource.stream()
+ private val spaceBackstack = ArrayDeque()
+
fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? {
// XXX we should somehow make it live :/ just a work around
// For example just after creating a space and switching to it the
@@ -87,12 +89,16 @@ class AppStateHandler @Inject constructor(
}
}
- fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) {
+ fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) {
+ val currentSpace = (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.space()
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
- if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace &&
- spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
+ if (currentSpace != null && spaceId == currentSpace.roomId) return
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
+ if (isForwardNavigation) {
+ spaceBackstack.addLast(currentSpace?.roomId)
+ }
+
if (persistNow) {
uiStateRepository.storeGroupingMethod(true, uSession.sessionId)
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
@@ -151,6 +157,8 @@ class AppStateHandler @Inject constructor(
}.launchIn(session.coroutineScope)
}
+ fun getSpaceBackstack() = spaceBackstack
+
fun safeActiveSpaceId(): String? {
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.spaceSummary?.roomId
}
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 3dba8b797b..e76f0ad672 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -101,6 +101,9 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
+import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedLoginFragment
+import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedRegisterFragment
+import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment
@@ -521,6 +524,21 @@ interface FragmentModule {
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
+ @Binds
+ @IntoMap
+ @FragmentKey(FtueAuthCombinedLoginFragment::class)
+ fun bindFtueAuthCombinedLoginFragment(fragment: FtueAuthCombinedLoginFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(FtueAuthCombinedRegisterFragment::class)
+ fun bindFtueAuthCombinedRegisterFragment(fragment: FtueAuthCombinedRegisterFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(FtueAuthCombinedServerSelectionFragment::class)
+ fun bindFtueAuthCombinedServerSelectionFragment(fragment: FtueAuthCombinedServerSelectionFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(UserListFragment::class)
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 33afcf1dfb..313fd7b98c 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel
+import im.vector.app.features.location.live.map.LocationLiveMapViewModel
import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
@@ -600,4 +601,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(LocationLiveMapViewModel::class)
+ fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
index a0c59038b0..085a9a5d12 100644
--- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -33,6 +33,7 @@ import im.vector.app.config.analyticsConfig
import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock
import im.vector.app.features.analytics.AnalyticsConfig
@@ -186,4 +187,8 @@ object VectorStaticModule {
fun providesAnalyticsConfig(): AnalyticsConfig {
return analyticsConfig
}
+
+ @Provides
+ @Singleton
+ fun providesBuildMeta() = BuildMeta()
}
diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt
index 648e441711..d50fe0ceab 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt
@@ -16,9 +16,13 @@
package im.vector.app.core.extensions
+import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import android.net.Uri
+import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ImageSpan
@@ -27,11 +31,13 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import dagger.hilt.EntryPoints
import im.vector.app.core.datastore.dataStoreProvider
import im.vector.app.core.di.SingletonEntryPoint
+import im.vector.app.core.resources.BuildMeta
import java.io.OutputStream
import kotlin.math.roundToInt
@@ -78,3 +84,31 @@ val Context.dataStoreProvider: (String) -> DataStore by dataStorePr
fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
return contentResolver.openOutputStream(uri, "wt")
}
+
+/**
+ * Checks for an active connection to infer if the device is offline.
+ * This is useful for breaking down UnknownHost exceptions and should not be used to determine if a valid connection is present
+ *
+ * @return true if no active connection is found
+ */
+@Suppress("deprecation")
+@SuppressLint("NewApi") // false positive
+fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean {
+ val connectivityManager = getSystemService()!!
+ return if (buildMeta.sdkInt > Build.VERSION_CODES.M) {
+ val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ when {
+ networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false
+ networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> false
+ networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true -> false
+ else -> true
+ }
+ } else {
+ when (connectivityManager.activeNetworkInfo?.type) {
+ ConnectivityManager.TYPE_WIFI -> false
+ ConnectivityManager.TYPE_MOBILE -> false
+ ConnectivityManager.TYPE_VPN -> false
+ else -> true
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
index dfbd2eba97..61c4fe2174 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt
@@ -36,9 +36,10 @@ fun Fragment.registerStartForActivityResult(onResult: (ActivityResult) -> Unit):
fun Fragment.addFragment(
frameId: Int,
fragment: Fragment,
+ tag: String? = null,
allowStateLoss: Boolean = false
) {
- parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) }
+ parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment, tag) }
}
fun Fragment.addFragment(
diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt
index 1884909906..2a2198d96b 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt
@@ -66,7 +66,7 @@ fun Session.startSyncing(context: Context) {
*/
fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0 &&
- cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
+ cryptoService().keysBackupService().getState() != KeysBackupState.ReadyToBackUp
}
fun Session.cannotLogoutSafely(): Boolean {
diff --git a/vector/src/main/java/im/vector/app/core/extensions/String.kt b/vector/src/main/java/im/vector/app/core/extensions/String.kt
new file mode 100644
index 0000000000..f035de469c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/extensions/String.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.extensions
+
+private const val RTL_OVERRIDE_CHAR = '\u202E'
+private const val LTR_OVERRIDE_CHAR = '\u202D'
+
+fun String.ensureEndsLeftToRight() = if (containsRtLOverride()) "$this$LTR_OVERRIDE_CHAR" else this
+
+fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR)
+
+fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR }
diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
index b1bb4c7d88..c89dc7a73c 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt
@@ -21,7 +21,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.AppNameProvider
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.StringProvider
-import org.matrix.android.sdk.api.session.pushers.PushersService
+import org.matrix.android.sdk.api.session.pushers.HttpPusher
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
@@ -55,7 +55,7 @@ class PushersManager @Inject constructor(
currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey))
}
- private fun createHttpPusher(pushKey: String) = PushersService.HttpPusher(
+ private fun createHttpPusher(pushKey: String) = HttpPusher(
pushKey,
stringProvider.getString(R.string.pusher_app_id),
profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()),
diff --git a/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt b/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt
new file mode 100644
index 0000000000..14d97e4c8f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.core.resources
+
+import android.os.Build
+
+data class BuildMeta(
+ val sdkInt: Int = Build.VERSION.SDK_INT
+)
diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
index d39dcbe318..09bb78c926 100644
--- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
@@ -56,4 +56,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
fun showLiveSenderInfo(): Boolean {
return vectorPreferences.showLiveSenderInfo()
}
+
+ fun autoplayAnimatedImages(): Boolean {
+ return vectorPreferences.autoplayAnimatedImages()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt
index 97c1dd32b5..2b7da49a71 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt
@@ -28,8 +28,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.toMatrixItem
-private const val MAX_RECEIPT_DISPLAYED = 5
-private const val MAX_RECEIPT_DESCRIBED = 3
+private const val MAX_RECEIPT_DISPLAYED = 3
class ReadReceiptsView @JvmOverloads constructor(
context: Context,
@@ -45,13 +44,7 @@ class ReadReceiptsView @JvmOverloads constructor(
}
private val receiptAvatars: List by lazy {
- listOf(
- views.receiptAvatar1,
- views.receiptAvatar2,
- views.receiptAvatar3,
- views.receiptAvatar4,
- views.receiptAvatar5
- )
+ listOf(views.receiptAvatar1, views.receiptAvatar2, views.receiptAvatar3)
}
private fun setupView() {
@@ -60,66 +53,58 @@ class ReadReceiptsView @JvmOverloads constructor(
}
fun render(readReceipts: List, avatarRenderer: AvatarRenderer) {
- if (readReceipts.isNotEmpty()) {
- isVisible = true
- for (index in 0 until MAX_RECEIPT_DISPLAYED) {
- val receiptData = readReceipts.getOrNull(index)
- if (receiptData == null) {
- receiptAvatars[index].visibility = View.INVISIBLE
- } else {
- receiptAvatars[index].visibility = View.VISIBLE
- avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index])
- }
- }
+ receiptAvatars.forEach { it.isVisible = false }
- val displayNames = readReceipts
- .mapNotNull { it.displayName }
- .filter { it.isNotBlank() }
- .take(MAX_RECEIPT_DESCRIBED)
+ readReceipts.take(MAX_RECEIPT_DISPLAYED).forEachIndexed { index, receiptData ->
+ receiptAvatars[index].isVisible = true
+ avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index])
+ }
- if (readReceipts.size > MAX_RECEIPT_DISPLAYED) {
- views.receiptMore.visibility = View.VISIBLE
- views.receiptMore.text = context.getString(
- R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED
- )
- } else {
- views.receiptMore.visibility = View.GONE
- }
- contentDescription = when (readReceipts.size) {
- 1 ->
- if (displayNames.size == 1) {
- context.getString(R.string.one_user_read, displayNames[0])
- } else {
- context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
- }
- 2 ->
- if (displayNames.size == 2) {
- context.getString(R.string.two_users_read, displayNames[0], displayNames[1])
- } else {
- context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
- }
- 3 ->
- if (displayNames.size == 3) {
- context.getString(R.string.three_users_read, displayNames[0], displayNames[1], displayNames[2])
- } else {
- context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
- }
- else ->
- if (displayNames.size >= 2) {
- val qty = readReceipts.size - 2
- context.resources.getQuantityString(
- R.plurals.two_and_some_others_read,
- qty,
- displayNames[0],
- displayNames[1],
- qty
- )
- } else {
- context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
- }
- }
+ val displayNames = readReceipts
+ .mapNotNull { it.displayName }
+ .filter { it.isNotBlank() }
+ .take(MAX_RECEIPT_DISPLAYED)
+
+ if (readReceipts.size > MAX_RECEIPT_DISPLAYED) {
+ views.receiptMore.visibility = View.VISIBLE
+ views.receiptMore.text = context.getString(
+ R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED
+ )
} else {
- isVisible = false
+ views.receiptMore.visibility = View.GONE
+ }
+ contentDescription = when (readReceipts.size) {
+ 1 ->
+ if (displayNames.size == 1) {
+ context.getString(R.string.one_user_read, displayNames[0])
+ } else {
+ context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
+ }
+ 2 ->
+ if (displayNames.size == 2) {
+ context.getString(R.string.two_users_read, displayNames[0], displayNames[1])
+ } else {
+ context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
+ }
+ 3 ->
+ if (displayNames.size == 3) {
+ context.getString(R.string.three_users_read, displayNames[0], displayNames[1], displayNames[2])
+ } else {
+ context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
+ }
+ else ->
+ if (displayNames.size >= 2) {
+ val qty = readReceipts.size - 2
+ context.resources.getQuantityString(
+ R.plurals.two_and_some_others_read,
+ qty,
+ displayNames[0],
+ displayNames[1],
+ qty
+ )
+ } else {
+ context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
index d6a63dca10..3e82ecd5f2 100644
--- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt
@@ -16,7 +16,7 @@
package im.vector.app.core.utils
-import com.vanniktech.emoji.EmojiUtils
+import com.vanniktech.emoji.isOnlyEmojis
/**
* Test if a string contains emojis.
@@ -28,7 +28,7 @@ import com.vanniktech.emoji.EmojiUtils
*/
fun containsOnlyEmojis(str: String?): Boolean {
// Now rely on vanniktech library
- return EmojiUtils.isOnlyEmojis(str)
+ return str.isOnlyEmojis()
}
/**
diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
index a89b167c55..9ad95d3c55 100644
--- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
@@ -19,7 +19,6 @@ package im.vector.app.core.utils
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
-import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -43,11 +42,6 @@ val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS)
val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
-val PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
-} else {
- PERMISSIONS_EMPTY
-}
// This is not ideal to store the value like that, but it works
private var permissionDialogDisplayed = false
diff --git a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
index eb6cf8e554..d6af3f5afb 100644
--- a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.os.Build
import android.text.format.Formatter
import im.vector.app.R
+import im.vector.app.core.resources.StringProvider
import org.threeten.bp.Duration
import java.util.TreeMap
@@ -60,10 +61,10 @@ object TextUtils {
} else {
// First convert the size
when {
- sizeBytes < 1024 -> sizeBytes
- sizeBytes < 1024 * 1024 -> sizeBytes * 1000 / 1024
+ sizeBytes < 1024 -> sizeBytes
+ sizeBytes < 1024 * 1024 -> sizeBytes * 1000 / 1024
sizeBytes < 1024 * 1024 * 1024 -> sizeBytes * 1000 / 1024 * 1000 / 1024
- else -> sizeBytes * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
+ else -> sizeBytes * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
}
}
@@ -85,50 +86,66 @@ object TextUtils {
}
}
- fun formatDurationWithUnits(context: Context, duration: Duration): String {
+ fun formatDurationWithUnits(context: Context, duration: Duration, appendSeconds: Boolean = true): String {
+ return formatDurationWithUnits(duration, context::getString, appendSeconds)
+ }
+
+ fun formatDurationWithUnits(stringProvider: StringProvider, duration: Duration, appendSeconds: Boolean = true): String {
+ return formatDurationWithUnits(duration, stringProvider::getString, appendSeconds)
+ }
+
+ /**
+ * We don't always have Context to get strings or we want to use StringProvider instead.
+ * So we can pass the getString function either from Context or the StringProvider.
+ * @param duration duration to be formatted
+ * @param getString getString method from Context or StringProvider
+ * @param appendSeconds if false than formatter will not append seconds
+ * @return formatted duration with a localized form like "10h 30min 5sec"
+ */
+ private fun formatDurationWithUnits(duration: Duration, getString: ((Int) -> String), appendSeconds: Boolean = true): String {
val hours = getHours(duration)
val minutes = getMinutes(duration)
val seconds = getSeconds(duration)
val builder = StringBuilder()
when {
- hours > 0 -> {
- appendHours(context, builder, hours)
+ hours > 0 -> {
+ appendHours(getString, builder, hours)
if (minutes > 0) {
builder.append(" ")
- appendMinutes(context, builder, minutes)
+ appendMinutes(getString, builder, minutes)
}
- if (seconds > 0) {
+ if (appendSeconds && seconds > 0) {
builder.append(" ")
- appendSeconds(context, builder, seconds)
+ appendSeconds(getString, builder, seconds)
}
}
minutes > 0 -> {
- appendMinutes(context, builder, minutes)
- if (seconds > 0) {
+ appendMinutes(getString, builder, minutes)
+ if (appendSeconds && seconds > 0) {
builder.append(" ")
- appendSeconds(context, builder, seconds)
+ appendSeconds(getString, builder, seconds)
}
}
- else -> {
- appendSeconds(context, builder, seconds)
+ else -> {
+ appendSeconds(getString, builder, seconds)
}
}
return builder.toString()
}
- private fun appendHours(context: Context, builder: StringBuilder, hours: Int) {
+ private fun appendHours(getString: ((Int) -> String), builder: StringBuilder, hours: Int) {
builder.append(hours)
- builder.append(context.resources.getString(R.string.time_unit_hour_short))
+ builder.append(getString(R.string.time_unit_hour_short))
}
- private fun appendMinutes(context: Context, builder: StringBuilder, minutes: Int) {
+ private fun appendMinutes(getString: ((Int) -> String), builder: StringBuilder, minutes: Int) {
builder.append(minutes)
- builder.append(context.getString(R.string.time_unit_minute_short))
+ builder.append(getString(R.string.time_unit_minute_short))
}
- private fun appendSeconds(context: Context, builder: StringBuilder, seconds: Int) {
+ private fun appendSeconds(getString: ((Int) -> String), builder: StringBuilder, seconds: Int) {
builder.append(seconds)
- builder.append(context.getString(R.string.time_unit_second_short))
+ builder.append(getString(R.string.time_unit_second_short))
}
private fun getHours(duration: Duration): Int = duration.toHours().toInt()
diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
index e3fded2824..6a7a0865de 100644
--- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
+++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
@@ -26,6 +26,7 @@ interface VectorFeatures {
fun isOnboardingUseCaseEnabled(): Boolean
fun isOnboardingPersonalizeEnabled(): Boolean
fun isOnboardingCombinedRegisterEnabled(): Boolean
+ fun isOnboardingCombinedLoginEnabled(): Boolean
fun isScreenSharingEnabled(): Boolean
enum class OnboardingVariant {
@@ -42,5 +43,6 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false
override fun isOnboardingCombinedRegisterEnabled() = false
+ override fun isOnboardingCombinedLoginEnabled() = false
override fun isScreenSharingEnabled(): Boolean = true
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
index e7ae7c11f2..3c0d47c79c 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt
@@ -61,7 +61,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(
init {
setState {
this.copy(
- keysBackupState = keysBackupService.state,
+ keysBackupState = keysBackupService.getState(),
keysBackupVersion = keysBackupService.keysBackupVersion
)
}
@@ -207,7 +207,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(
}
fun canExit(): Boolean {
- val currentBackupState = keysBackupService.state
+ val currentBackupState = keysBackupService.getState()
return currentBackupState == KeysBackupState.Unknown ||
currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver
diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
index 88aad3277d..48679f6dbe 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt
@@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfo
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
-import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
@@ -284,7 +284,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
session.sharedSecretStorageService().storeSecret(
name = name,
secretBase64 = value,
- keys = listOf(SharedSecretStorageService.KeyRef(keyInfo.id, keySpec))
+ keys = listOf(KeyRef(keyInfo.id, keySpec))
)
decryptedSecretMap[name] = value
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt
index 28dbe2baa2..b5300ef700 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt
@@ -26,8 +26,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
-import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
@@ -142,7 +142,7 @@ class BackupToQuadSMigrationTask @Inject constructor(
quadS.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
curveKey.toBase64NoPadding(),
- listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec))
+ listOf(KeyRef(info.keyId, info.keySpec))
)
// save for gossiping
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt
index c26aec45dd..c7c367f5ec 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt
@@ -34,7 +34,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
-import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
import org.matrix.android.sdk.api.util.awaitCallback
@@ -67,6 +67,7 @@ data class Params(
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?,
val keySpec: SsssKeySpec? = null,
+ val forceResetIfSomeSecretsAreMissing: Boolean = false,
val setupMode: SetupMode
)
@@ -83,6 +84,7 @@ class BootstrapCrossSigningTask @Inject constructor(
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() ||
+ (params.forceResetIfSomeSecretsAreMissing && !crossSigningService.allPrivateKeysKnown()) ||
(params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) ||
(params.setupMode == SetupMode.HARD_RESET)
if (shouldSetCrossSigning) {
@@ -183,7 +185,7 @@ class BootstrapCrossSigningTask @Inject constructor(
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
mskPrivateKey,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
params.progressListener?.onProgress(
WaitingViewData(
@@ -195,7 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor(
ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME,
uskPrivateKey,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
params.progressListener?.onProgress(
WaitingViewData(
@@ -206,7 +208,7 @@ class BootstrapCrossSigningTask @Inject constructor(
ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME,
sskPrivateKey,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
} catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
@@ -258,7 +260,7 @@ class BootstrapCrossSigningTask @Inject constructor(
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
}
} else {
@@ -275,7 +277,7 @@ class BootstrapCrossSigningTask @Inject constructor(
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
- listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
}
} else {
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
index 3be2f020b8..3d078a82ed 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
@@ -26,6 +26,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentBootstrapSetupRecoveryBinding
+import im.vector.app.features.raw.wellknown.SecureBackupMethod
import javax.inject.Inject
class BootstrapSetupRecoveryKeyFragment @Inject constructor() :
@@ -55,27 +56,40 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() :
}
override fun invalidate() = withState(sharedViewModel) { state ->
- if (state.step is BootstrapStep.FirstForm) {
- if (state.step.keyBackUpExist) {
- // Display the set up action
- views.bootstrapSetupSecureSubmit.isVisible = true
- views.bootstrapSetupSecureUseSecurityKey.isVisible = false
- views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
- views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
- } else {
- if (state.step.reset) {
- views.bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title)
- views.bootstrapSetupWarningTextView.isVisible = true
- } else {
- views.bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
- views.bootstrapSetupWarningTextView.isVisible = false
- }
- // Choose between create a passphrase or use a recovery key
- views.bootstrapSetupSecureSubmit.isVisible = false
- views.bootstrapSetupSecureUseSecurityKey.isVisible = true
- views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = true
- views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true
- }
+ val firstFormStep = state.step as? BootstrapStep.FirstForm ?: return@withState
+
+ if (firstFormStep.keyBackUpExist) {
+ renderStateWithExistingKeyBackup()
+ } else {
+ renderSetupHeader(needsReset = firstFormStep.reset)
+ views.bootstrapSetupSecureSubmit.isVisible = false
+
+ // Choose between create a passphrase or use a recovery key
+ renderBackupMethodActions(firstFormStep.methods)
}
}
+
+ private fun renderStateWithExistingKeyBackup() = with(views) {
+ // Display the set up action
+ bootstrapSetupSecureSubmit.isVisible = true
+ // Disable creating backup / passphrase options
+ bootstrapSetupSecureUseSecurityKey.isVisible = false
+ bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
+ bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
+ }
+
+ private fun renderSetupHeader(needsReset: Boolean) = with(views) {
+ bootstrapSetupSecureText.text = if (needsReset) {
+ getString(R.string.reset_secure_backup_title)
+ } else {
+ getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
+ }
+ bootstrapSetupWarningTextView.isVisible = needsReset
+ }
+
+ private fun renderBackupMethodActions(method: SecureBackupMethod) = with(views) {
+ bootstrapSetupSecureUseSecurityKey.isVisible = method.isKeyAvailable
+ bootstrapSetupSecureUseSecurityPassphrase.isVisible = method.isPassphraseAvailable
+ bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = method.isPassphraseAvailable
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
index 53ac3ade65..15ea90ae0a 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
@@ -33,6 +33,10 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
+import im.vector.app.features.raw.wellknown.SecureBackupMethod
+import im.vector.app.features.raw.wellknown.getElementWellknown
+import im.vector.app.features.raw.wellknown.isSecureBackupRequired
+import im.vector.app.features.raw.wellknown.secureBackupMethod
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
@@ -41,7 +45,9 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
@@ -61,6 +67,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val session: Session,
+ private val rawService: RawService,
private val bootstrapTask: BootstrapCrossSigningTask,
private val migrationTask: BackupToQuadSMigrationTask,
) : VectorViewModel(initialState) {
@@ -83,12 +90,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
init {
+ setState {
+ copy(step = BootstrapStep.CheckingMigration, isRecoverySetup = session.sharedSecretStorageService().isRecoverySetup())
+ }
+
+ // Refresh the well-known configuration
+ viewModelScope.launch(Dispatchers.IO) {
+ val wellKnown = rawService.getElementWellknown(session.sessionParams)
+ setState {
+ copy(
+ isSecureBackupRequired = wellKnown?.isSecureBackupRequired().orFalse(),
+ secureBackupMethod = wellKnown?.secureBackupMethod() ?: SecureBackupMethod.KEY_OR_PASSPHRASE,
+ )
+ }
+ }
+
when (initialState.setupMode) {
SetupMode.PASSPHRASE_RESET,
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
- SetupMode.HARD_RESET -> {
+ SetupMode.HARD_RESET -> {
setState {
- copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
+ copy(
+ step = BootstrapStep.FirstForm(
+ keyBackUpExist = false,
+ reset = session.sharedSecretStorageService().isRecoverySetup(),
+ methods = this.secureBackupMethod
+ )
+ )
}
}
SetupMode.CROSS_SIGNING_ONLY -> {
@@ -97,7 +125,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = BootstrapStep.AccountReAuth())
}
}
- SetupMode.NORMAL -> {
+ SetupMode.NORMAL -> {
// need to check if user have an existing keybackup
setState {
copy(step = BootstrapStep.CheckingMigration)
@@ -112,7 +140,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// we just resume plain bootstrap
doesKeyBackupExist = false
setState {
- copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
+ copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod))
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
@@ -126,7 +154,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
doesKeyBackupExist = true
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
setState {
- copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
+ copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod))
}
}
}
@@ -149,14 +177,14 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun handle(action: BootstrapActions) = withState { state ->
when (action) {
- is BootstrapActions.GoBack -> queryBack()
- BootstrapActions.StartKeyBackupMigration -> {
+ is BootstrapActions.GoBack -> queryBack()
+ BootstrapActions.StartKeyBackupMigration -> {
handleStartMigratingKeyBackup()
}
- is BootstrapActions.Start -> {
+ is BootstrapActions.Start -> {
handleStart(action)
}
- is BootstrapActions.UpdateCandidatePassphrase -> {
+ is BootstrapActions.UpdateCandidatePassphrase -> {
val strength = zxcvbn.measure(action.pass)
setState {
copy(
@@ -165,7 +193,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
- is BootstrapActions.GoToConfirmPassphrase -> {
+ is BootstrapActions.GoToConfirmPassphrase -> {
setState {
copy(
passphrase = action.passphrase,
@@ -180,7 +208,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
- is BootstrapActions.DoInitialize -> {
+ is BootstrapActions.DoInitialize -> {
if (state.passphrase == state.passphraseRepeat) {
startInitializeFlow(state)
} else {
@@ -191,42 +219,42 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
}
- is BootstrapActions.DoInitializeGeneratedKey -> {
+ is BootstrapActions.DoInitializeGeneratedKey -> {
startInitializeFlow(state)
}
- BootstrapActions.RecoveryKeySaved -> {
+ BootstrapActions.RecoveryKeySaved -> {
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
setState {
copy(step = BootstrapStep.SaveRecoveryKey(true))
}
}
- BootstrapActions.Completed -> {
+ BootstrapActions.Completed -> {
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
- BootstrapActions.GoToCompleted -> {
+ BootstrapActions.GoToCompleted -> {
setState {
copy(step = BootstrapStep.DoneSuccess)
}
}
- BootstrapActions.SaveReqQueryStarted -> {
+ BootstrapActions.SaveReqQueryStarted -> {
setState {
copy(recoverySaveFileProcess = Loading())
}
}
- is BootstrapActions.SaveKeyToUri -> {
+ is BootstrapActions.SaveKeyToUri -> {
saveRecoveryKeyToUri(action.os)
}
- BootstrapActions.SaveReqFailed -> {
+ BootstrapActions.SaveReqFailed -> {
setState {
copy(recoverySaveFileProcess = Uninitialized)
}
}
- BootstrapActions.GoToEnterAccountPassword -> {
+ BootstrapActions.GoToEnterAccountPassword -> {
setState {
copy(step = BootstrapStep.AccountReAuth())
}
}
- BootstrapActions.HandleForgotBackupPassphrase -> {
+ BootstrapActions.HandleForgotBackupPassphrase -> {
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(true))
@@ -236,16 +264,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// is BootstrapActions.ReAuth -> {
// startInitializeFlow(action.pass)
// }
- is BootstrapActions.DoMigrateWithPassphrase -> {
+ is BootstrapActions.DoMigrateWithPassphrase -> {
startMigrationFlow(state.step, action.passphrase, null)
}
- is BootstrapActions.DoMigrateWithRecoveryKey -> {
+ is BootstrapActions.DoMigrateWithRecoveryKey -> {
startMigrationFlow(state.step, null, action.recoveryKey)
}
- BootstrapActions.SsoAuthDone -> {
+ BootstrapActions.SsoAuthDone -> {
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
}
- is BootstrapActions.PasswordAuthDone -> {
+ is BootstrapActions.PasswordAuthDone -> {
val decryptedPass = session.secureStorageService()
.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
@@ -256,7 +284,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
)
}
- BootstrapActions.ReAuthCancelled -> {
+ BootstrapActions.ReAuthCancelled -> {
setState {
copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error)))
}
@@ -386,7 +414,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
- LoginFlowTypes.SSO -> {
+ LoginFlowTypes.SSO -> {
pendingAuth = DefaultBaseAuth(flowResponse.session)
uiaContinuation = promise
setState {
@@ -396,7 +424,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
- else -> {
+ else -> {
promise.resumeWithException(UnsupportedOperationException())
}
}
@@ -411,6 +439,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
+ forceResetIfSomeSecretsAreMissing = state.isSecureBackupRequired,
setupMode = state.setupMode
)
) { bootstrapResult ->
@@ -418,18 +447,26 @@ class BootstrapSharedViewModel @AssistedInject constructor(
is BootstrapResult.SuccessCrossSigningOnly -> {
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
- is BootstrapResult.Success -> {
- setState {
- copy(
- recoveryKeyCreationInfo = bootstrapResult.keyInfo,
- step = BootstrapStep.SaveRecoveryKey(
- // If a passphrase was used, saving key is optional
- state.passphrase != null
- )
- )
+ is BootstrapResult.Success -> {
+ val isSecureBackupRequired = state.isSecureBackupRequired
+ val secureBackupMethod = state.secureBackupMethod
+
+ if (state.passphrase != null && isSecureBackupRequired && secureBackupMethod == SecureBackupMethod.PASSPHRASE) {
+ // Go straight to conclusion, skip the save key step
+ _viewEvents.post(BootstrapViewEvents.Dismiss(success = true))
+ } else {
+ setState {
+ copy(
+ recoveryKeyCreationInfo = bootstrapResult.keyInfo,
+ step = BootstrapStep.SaveRecoveryKey(
+ // If a passphrase was used, saving key is optional
+ state.passphrase != null
+ )
+ )
+ }
}
}
- is BootstrapResult.InvalidPasswordError -> {
+ is BootstrapResult.InvalidPasswordError -> {
// it's a bad password / auth
setState {
copy(
@@ -437,7 +474,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
- is BootstrapResult.Failure -> {
+ is BootstrapResult.Failure -> {
if (bootstrapResult is BootstrapResult.GenericError &&
bootstrapResult.failure is Failure.OtherServerError &&
bootstrapResult.failure.httpCode == 401) {
@@ -476,7 +513,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} else {
setState {
copy(
- step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
+ step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null,
@@ -486,48 +523,64 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
}
- is BootstrapStep.SetupPassphrase -> {
+ is BootstrapStep.SetupPassphrase -> {
setState {
copy(
- step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
+ step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null
)
}
}
- is BootstrapStep.ConfirmPassphrase -> {
+ is BootstrapStep.ConfirmPassphrase -> {
setState {
copy(
step = BootstrapStep.SetupPassphrase
)
}
}
- is BootstrapStep.AccountReAuth -> {
- _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
+ is BootstrapStep.AccountReAuth -> {
+ if (state.canLeave) {
+ _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
+ } else {
+ // Go back to the first step
+ setState {
+ copy(
+ step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
+ // Also reset the passphrase
+ passphrase = null,
+ passphraseRepeat = null
+ )
+ }
+ }
}
- BootstrapStep.Initializing -> {
+ BootstrapStep.Initializing -> {
// do we let you cancel from here?
- _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
+ if (state.canLeave) {
+ _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
+ }
}
is BootstrapStep.SaveRecoveryKey,
- BootstrapStep.DoneSuccess -> {
+ BootstrapStep.DoneSuccess -> {
// nop
}
- BootstrapStep.CheckingMigration -> Unit
- is BootstrapStep.FirstForm -> {
- _viewEvents.post(
- when (state.setupMode) {
- SetupMode.CROSS_SIGNING_ONLY,
- SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap()
- else -> BootstrapViewEvents.Dismiss(success = false)
- }
- )
+ BootstrapStep.CheckingMigration -> Unit
+ is BootstrapStep.FirstForm -> {
+ if (state.canLeave) {
+ _viewEvents.post(
+ when (state.setupMode) {
+ SetupMode.CROSS_SIGNING_ONLY,
+ SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap()
+ else -> BootstrapViewEvents.Dismiss(success = false)
+ }
+ )
+ }
}
- is BootstrapStep.GetBackupSecretForMigration -> {
+ is BootstrapStep.GetBackupSecretForMigration -> {
setState {
copy(
- step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
+ step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null,
@@ -542,10 +595,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private fun BackupToQuadSMigrationTask.Result.Failure.toHumanReadable(): String {
return when (this) {
is BackupToQuadSMigrationTask.Result.InvalidRecoverySecret -> stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
- is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
+ is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
// is BackupToQuadSMigrationTask.Result.NoKeyBackupVersion,
// is BackupToQuadSMigrationTask.Result.IllegalParams,
- else -> stringProvider.getString(R.string.unexpected_error)
+ else -> stringProvider.getString(R.string.unexpected_error)
}
}
}
+
+private val BootstrapViewState.canLeave: Boolean get() = !isSecureBackupRequired || isRecoverySetup
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt
index ed8f5b92d7..3975f0e9a2 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt
@@ -16,6 +16,8 @@
package im.vector.app.features.crypto.recover
+import im.vector.app.features.raw.wellknown.SecureBackupMethod
+
/**
* TODO The schema is not up to date
*
@@ -89,7 +91,7 @@ sealed class BootstrapStep {
object CheckingMigration : BootstrapStep()
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
- data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
+ data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false, val methods: SecureBackupMethod) : BootstrapStep()
object SetupPassphrase : BootstrapStep()
object ConfirmPassphrase : BootstrapStep()
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt
index 9d5760cbf9..2d27c165e6 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt
@@ -21,6 +21,7 @@ import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import com.nulabinc.zxcvbn.Strength
import im.vector.app.core.platform.WaitingViewData
+import im.vector.app.features.raw.wellknown.SecureBackupMethod
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
data class BootstrapViewState(
@@ -34,7 +35,10 @@ data class BootstrapViewState(
val passphraseConfirmMatch: Async = Uninitialized,
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
val initializationWaitingViewData: WaitingViewData? = null,
- val recoverySaveFileProcess: Async = Uninitialized
+ val recoverySaveFileProcess: Async = Uninitialized,
+ val isSecureBackupRequired: Boolean = false,
+ val secureBackupMethod: SecureBackupMethod = SecureBackupMethod.KEY_OR_PASSPHRASE,
+ val isRecoverySetup: Boolean = true
) : MavericksState {
constructor(args: BootstrapBottomSheet.Args) : this(setupMode = args.setUpMode)
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
index a5142ad8bf..c4ae2d278b 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
@@ -27,7 +27,7 @@ sealed class VerificationAction : VectorViewModelAction {
object OtherUserDidNotScanned : VerificationAction()
data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
- object GotItConclusion : VerificationAction()
+ data class GotItConclusion(val verified: Boolean) : VerificationAction()
object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
index f5818ce2f4..b82ef1d164 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
@@ -30,9 +30,13 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.raw.wellknown.getElementWellknown
+import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@@ -78,6 +82,7 @@ data class VerificationBottomSheetViewState(
val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false,
val quadSContainsSecrets: Boolean = true,
+ val isVerificationRequired: Boolean = false,
val quadSHasBeenReset: Boolean = false,
val hasAnyOtherSession: Boolean = false
) : MavericksState {
@@ -92,6 +97,7 @@ data class VerificationBottomSheetViewState(
class VerificationBottomSheetViewModel @AssistedInject constructor(
@Assisted initialState: VerificationBottomSheetViewState,
+ private val rawService: RawService,
private val session: Session,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stringProvider: StringProvider
@@ -109,6 +115,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
init {
session.cryptoService().verificationService().addListener(this)
+ // This is async, but at this point should be in cache
+ // so it's ok to not wait until result
+ viewModelScope.launch(Dispatchers.IO) {
+ val wellKnown = rawService.getElementWellknown(session.sessionParams)
+ setState {
+ copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse())
+ }
+ }
+
val userItem = session.getUser(initialState.otherUserId)
var autoReady = false
@@ -183,8 +198,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
state.verifyingFrom4S) {
// you cannot cancel anymore
} else {
- setState {
- copy(userWantsToCancel = true)
+ if (!state.isVerificationRequired) {
+ setState {
+ copy(userWantsToCancel = true)
+ }
}
}
}
@@ -235,7 +252,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
?: session.roomService().getExistingDirectRoomWithUser(otherUserId)
when (action) {
- is VerificationAction.RequestVerificationByDM -> {
+ is VerificationAction.RequestVerificationByDM -> {
if (roomId == null) {
val localId = LocalEcho.createLocalEchoId()
setState {
@@ -286,7 +303,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
Unit
}
- is VerificationAction.StartSASVerification -> {
+ is VerificationAction.StartSASVerification -> {
val request = session.cryptoService().verificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
?: return@withState
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
@@ -308,7 +325,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
Unit
}
- is VerificationAction.RemoteQrCodeScanned -> {
+ is VerificationAction.RemoteQrCodeScanned -> {
val existingTransaction = session.cryptoService().verificationService()
.getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
existingTransaction
@@ -322,7 +339,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
existingTransaction
?.otherUserScannedMyQrCode()
}
- is VerificationAction.OtherUserDidNotScanned -> {
+ is VerificationAction.OtherUserDidNotScanned -> {
val transactionId = state.transactionId ?: return@withState
val existingTransaction = session.cryptoService().verificationService()
@@ -330,31 +347,42 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
existingTransaction
?.otherUserDidNotScannedMyQrCode()
}
- is VerificationAction.SASMatchAction -> {
+ is VerificationAction.SASMatchAction -> {
(session.cryptoService().verificationService()
.getExistingTransaction(action.otherUserId, action.sasTransactionId)
as? SasVerificationTransaction)?.userHasVerifiedShortCode()
}
- is VerificationAction.SASDoNotMatchAction -> {
+ is VerificationAction.SASDoNotMatchAction -> {
(session.cryptoService().verificationService()
.getExistingTransaction(action.otherUserId, action.sasTransactionId)
as? SasVerificationTransaction)
?.shortCodeDoesNotMatch()
}
- is VerificationAction.GotItConclusion -> {
+ is VerificationAction.GotItConclusion -> {
+ if (state.isVerificationRequired && !action.verified) {
+ // we should go back to first screen
+ setState {
+ copy(
+ pendingRequest = Uninitialized,
+ sasTransactionState = null,
+ qrTransactionState = null
+ )
+ }
+ } else {
+ _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
+ }
+ }
+ is VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
- is VerificationAction.SkipVerification -> {
- _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
- }
- is VerificationAction.VerifyFromPassphrase -> {
+ is VerificationAction.VerifyFromPassphrase -> {
setState { copy(verifyingFrom4S = true) }
_viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
}
- is VerificationAction.GotResultFromSsss -> {
+ is VerificationAction.GotResultFromSsss -> {
handleSecretBackFromSSSS(action)
}
- VerificationAction.SecuredStorageHasBeenReset -> {
+ VerificationAction.SecuredStorageHasBeenReset -> {
if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) {
setState {
copy(quadSHasBeenReset = true, verifyingFrom4S = false)
@@ -362,7 +390,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
Unit
}
- VerificationAction.CancelledFromSsss -> {
+ VerificationAction.CancelledFromSsss -> {
setState {
copy(verifyingFrom4S = false)
}
@@ -482,7 +510,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
when (tx) {
- is SasVerificationTransaction -> {
+ is SasVerificationTransaction -> {
if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
// A SAS tx has been started following this request
setState {
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
index 7cec57c8af..9c5829eb8e 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt
@@ -84,7 +84,7 @@ class VerificationConclusionController @Inject constructor(
notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence())
}
- bottomDone()
+ bottomGotIt()
}
ConclusionState.CANCELLED -> {
bottomSheetVerificationNoticeItem {
@@ -92,18 +92,7 @@ class VerificationConclusionController @Inject constructor(
notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence())
}
- bottomSheetDividerItem {
- id("sep0")
- }
-
- bottomSheetVerificationActionItem {
- id("got_it")
- title(host.stringProvider.getString(R.string.sas_got_it))
- titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
- iconRes(R.drawable.ic_arrow_right)
- iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
- listener { host.listener?.onButtonTapped() }
- }
+ bottomGotIt()
}
}
}
@@ -120,11 +109,27 @@ class VerificationConclusionController @Inject constructor(
titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
- listener { host.listener?.onButtonTapped() }
+ listener { host.listener?.onButtonTapped(true) }
+ }
+ }
+
+ private fun bottomGotIt() {
+ val host = this
+ bottomSheetDividerItem {
+ id("sep0")
+ }
+
+ bottomSheetVerificationActionItem {
+ id("got_it")
+ title(host.stringProvider.getString(R.string.sas_got_it))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener { host.listener?.onButtonTapped(false) }
}
}
interface Listener {
- fun onButtonTapped()
+ fun onButtonTapped(success: Boolean)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt
index f45bc3d44e..85b90e6004 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt
@@ -73,7 +73,7 @@ class VerificationConclusionFragment @Inject constructor(
controller.update(state)
}
- override fun onButtonTapped() {
- sharedViewModel.handle(VerificationAction.GotItConclusion)
+ override fun onButtonTapped(success: Boolean) {
+ sharedViewModel.handle(VerificationAction.GotItConclusion(success))
}
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
index 8c675ecf3e..e050c5bfb0 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt
@@ -88,17 +88,19 @@ class VerificationRequestController @Inject constructor(
}
}
- bottomSheetDividerItem {
- id("sep1")
- }
+ if (!state.isVerificationRequired) {
+ bottomSheetDividerItem {
+ id("sep1")
+ }
- bottomSheetVerificationActionItem {
- id("skip")
- title(host.stringProvider.getString(R.string.action_skip))
- titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
- iconRes(R.drawable.ic_arrow_right)
- iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
- listener { host.listener?.onClickSkip() }
+ bottomSheetVerificationActionItem {
+ id("skip")
+ title(host.stringProvider.getString(R.string.action_skip))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
+ listener { host.listener?.onClickSkip() }
+ }
}
} else {
val styledText =
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index f2d3011471..9fe1e00ae7 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -50,6 +50,7 @@ import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.plan.ViewRoom
+import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo
@@ -197,45 +198,15 @@ class HomeActivity :
.stream()
.onEach { sharedAction ->
when (sharedAction) {
- is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START)
- is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START)
- is HomeActivitySharedAction.OpenGroup -> {
- views.drawerLayout.closeDrawer(GravityCompat.START)
-
- // Temporary
- // When switching from space to group or group to space, we need to reload the fragment
- // To be removed when dropping legacy groups
- if (sharedAction.clearFragment) {
- replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
- } else {
- // nop
- }
- // we might want to delay that to avoid having the drawer animation lagging
- // would be probably better to let the drawer do that? in the on closed callback?
- }
- is HomeActivitySharedAction.OpenSpacePreview -> {
- startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
- }
- is HomeActivitySharedAction.AddSpace -> {
- createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
- }
- is HomeActivitySharedAction.ShowSpaceSettings -> {
- // open bottom sheet
- SpaceSettingsMenuBottomSheet
- .newInstance(sharedAction.spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
- override fun onShareSpaceSelected(spaceId: String) {
- ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
- }
- })
- .show(supportFragmentManager, "SPACE_SETTINGS")
- }
- is HomeActivitySharedAction.OpenSpaceInvite -> {
- SpaceInviteBottomSheet.newInstance(sharedAction.spaceId)
- .show(supportFragmentManager, "SPACE_INVITE")
- }
- HomeActivitySharedAction.SendSpaceFeedBack -> {
- bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
- }
+ is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START)
+ is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START)
+ is HomeActivitySharedAction.OpenGroup -> openGroup(sharedAction.shouldClearFragment)
+ is HomeActivitySharedAction.OpenSpacePreview -> startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
+ is HomeActivitySharedAction.AddSpace -> createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
+ is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId)
+ is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId)
+ HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
+ HomeActivitySharedAction.CloseGroup -> closeGroup()
}
}
.launchIn(lifecycleScope)
@@ -254,12 +225,20 @@ class HomeActivity :
homeActivityViewModel.observeViewEvents {
when (it) {
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
- is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
- HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
- is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
- HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
- HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
- is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
+ is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
+ HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
+ HomeActivityViewEvents.StartRecoverySetupFlow -> handleStartRecoverySetup()
+ is HomeActivityViewEvents.ForceVerification -> {
+ if (it.sendRequest) {
+ navigator.requestSelfSessionVerification(this)
+ } else {
+ navigator.waitSessionVerification(this)
+ }
+ }
+ is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
+ HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
+ HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
+ is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
}
}
homeActivityViewModel.onEach { renderState(it) }
@@ -272,6 +251,37 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
+ private fun openGroup(shouldClearFragment: Boolean) {
+ views.drawerLayout.closeDrawer(GravityCompat.START)
+
+ // When switching from space to group or group to space, we need to reload the fragment
+ if (shouldClearFragment) {
+ replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
+ } else {
+ // do nothing
+ }
+ }
+
+ private fun showSpaceSettings(spaceId: String) {
+ // open bottom sheet
+ SpaceSettingsMenuBottomSheet
+ .newInstance(spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
+ override fun onShareSpaceSelected(spaceId: String) {
+ ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
+ }
+ })
+ .show(supportFragmentManager, "SPACE_SETTINGS")
+ }
+
+ private fun openSpaceInvite(spaceId: String) {
+ SpaceInviteBottomSheet.newInstance(spaceId)
+ .show(supportFragmentManager, "SPACE_INVITE")
+ }
+
+ private fun closeGroup() {
+ views.drawerLayout.openDrawer(GravityCompat.START)
+ }
+
private fun handleShowAnalyticsOptIn() {
navigator.openAnalyticsOptIn(this)
}
@@ -326,12 +336,12 @@ class HomeActivity :
when {
deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length)
deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length)
- else -> null
+ else -> null
}?.let { permalinkId ->
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(permalinkId)
}
}
- else -> deepLink
+ else -> deepLink
}
lifecycleScope.launch {
@@ -354,6 +364,13 @@ class HomeActivity :
}
}
+ private fun handleStartRecoverySetup() {
+ // To avoid IllegalStateException in case the transaction was executed after onSaveInstanceState
+ lifecycleScope.launchWhenResumed {
+ navigator.open4SSetup(this@HomeActivity, SetupMode.NORMAL)
+ }
+ }
+
private fun renderState(state: HomeActivityViewState) {
when (val status = state.syncStatusServiceStatus) {
is SyncStatusService.Status.InitialSyncProgressing -> {
@@ -374,7 +391,7 @@ class HomeActivity :
}
views.waitingView.root.isVisible = true
}
- else -> {
+ else -> {
// Idle or Incremental sync status
views.waitingView.root.isVisible = false
}
@@ -526,15 +543,15 @@ class HomeActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
- R.id.menu_home_suggestion -> {
+ R.id.menu_home_suggestion -> {
bugReporter.openBugReportScreen(this, ReportType.SUGGESTION)
return true
}
- R.id.menu_home_report_bug -> {
+ R.id.menu_home_report_bug -> {
bugReporter.openBugReportScreen(this, ReportType.BUG_REPORT)
return true
}
- R.id.menu_home_init_sync_legacy -> {
+ R.id.menu_home_init_sync_legacy -> {
// Configure the SDK
initialSyncStrategy = InitialSyncStrategy.Legacy
// And clear cache
@@ -548,11 +565,11 @@ class HomeActivity :
MainActivity.restartApp(this, MainActivityArgs(clearCache = true))
return true
}
- R.id.menu_home_filter -> {
+ R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this)
return true
}
- R.id.menu_home_setting -> {
+ R.id.menu_home_setting -> {
navigator.openSettings(this)
return true
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt
index d460efb564..a16d710f4e 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt
@@ -24,7 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
- data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction()
+ data class OpenGroup(val shouldClearFragment: Boolean) : HomeActivitySharedAction()
+ object CloseGroup : HomeActivitySharedAction()
object AddSpace : HomeActivitySharedAction()
data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction()
data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
index 5efd49a579..cb31a568e4 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
@@ -27,4 +27,6 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
object ShowAnalyticsOptIn : HomeActivityViewEvents
object NotifyUserForThreadsMigration : HomeActivityViewEvents
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
+ object StartRecoverySetupFlow : HomeActivityViewEvents
+ data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 32f5a68377..9fe8a1f60e 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -17,7 +17,9 @@
package im.vector.app.features.home
import androidx.lifecycle.asFlow
+import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -28,6 +30,9 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.store.AnalyticsStore
import im.vector.app.features.login.ReAuthHelper
+import im.vector.app.features.raw.wellknown.ElementWellKnown
+import im.vector.app.features.raw.wellknown.getElementWellknown
+import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
@@ -42,6 +47,8 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.getUser
@@ -59,8 +66,9 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class HomeActivityViewModel @AssistedInject constructor(
- @Assisted initialState: HomeActivityViewState,
+ @Assisted private val initialState: HomeActivityViewState,
private val activeSessionHolder: ActiveSessionHolder,
+ private val rawService: RawService,
private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage,
@@ -72,10 +80,17 @@ class HomeActivityViewModel @AssistedInject constructor(
override fun create(initialState: HomeActivityViewState): HomeActivityViewModel
}
- companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
+ override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? {
+ val activity: HomeActivity = viewModelContext.activity()
+ val args: HomeActivityArgs? = activity.intent.getParcelableExtra(Mavericks.KEY_ARG)
+ return args?.let { HomeActivityViewState(accountCreation = it.accountCreation) }
+ ?: super.initialState(viewModelContext)
+ }
+ }
private var isInitialized = false
- private var checkBootstrap = false
+ private var hasCheckedBootstrap = false
private var onceTrusted = false
private fun initialize() {
@@ -116,17 +131,13 @@ class HomeActivityViewModel @AssistedInject constructor(
safeActiveSession
.flow()
.liveCrossSigningInfo(safeActiveSession.myUserId)
- .onEach {
- val isVerified = it.getOrNull()?.isTrusted() ?: false
+ .onEach { info ->
+ val isVerified = info.getOrNull()?.isTrusted() ?: false
if (!isVerified && onceTrusted) {
- // cross signing keys have been reset
- // Trigger a popup to re-verify
- // Note: user can be null in case of logout
- safeActiveSession.getUser(safeActiveSession.myUserId)
- ?.toMatrixItem()
- ?.let { user ->
- _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user))
- }
+ viewModelScope.launch(Dispatchers.IO) {
+ val elementWellKnown = rawService.getElementWellknown(safeActiveSession.sessionParams)
+ sessionHasBeenUnverified(elementWellKnown)
+ }
}
onceTrusted = isVerified
}
@@ -156,7 +167,7 @@ class HomeActivityViewModel @AssistedInject constructor(
vectorPreferences.userNotifiedAboutThreads()
}
// Migrate users with enabled lab settings
- vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.shouldMigrateThreads() -> {
+ vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.shouldMigrateThreads() -> {
Timber.i("----> Migrate threads with enabled labs")
// If user had io.element.thread enabled then enable the new thread support,
// clear cache to sync messages appropriately
@@ -166,7 +177,7 @@ class HomeActivityViewModel @AssistedInject constructor(
_viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false))
}
// Enable all users
- vectorPreferences.shouldMigrateThreads() && vectorPreferences.areThreadMessagesEnabled() -> {
+ vectorPreferences.shouldMigrateThreads() && vectorPreferences.areThreadMessagesEnabled() -> {
Timber.i("----> Try to migrate threads")
_viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = true))
}
@@ -180,17 +191,10 @@ class HomeActivityViewModel @AssistedInject constructor(
.asFlow()
.onEach { status ->
when (status) {
- is SyncStatusService.Status.InitialSyncProgressing -> {
- // Schedule a check of the bootstrap when the init sync will be finished
- checkBootstrap = true
+ is SyncStatusService.Status.Idle -> {
+ maybeVerifyOrBootstrapCrossSigning()
}
- is SyncStatusService.Status.Idle -> {
- if (checkBootstrap) {
- checkBootstrap = false
- maybeBootstrapCrossSigningAfterInitialSync()
- }
- }
- else -> Unit
+ else -> Unit
}
setState {
@@ -200,6 +204,10 @@ class HomeActivityViewModel @AssistedInject constructor(
}
}
.launchIn(viewModelScope)
+
+ if (session.hasAlreadySynced()) {
+ maybeVerifyOrBootstrapCrossSigning()
+ }
}
/**
@@ -240,12 +248,72 @@ class HomeActivityViewModel @AssistedInject constructor(
}
}
- private fun maybeBootstrapCrossSigningAfterInitialSync() {
+ private fun sessionHasBeenUnverified(elementWellKnown: ElementWellKnown?) {
+ val session = activeSessionHolder.getSafeActiveSession() ?: return
+ val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
+ if (isSecureBackupRequired) {
+ // If 4S is forced, force verification
+ // for stability cancel all pending verifications?
+ session.cryptoService().verificationService().getExistingVerificationRequests(session.myUserId).forEach {
+ session.cryptoService().verificationService().cancelVerificationRequest(it)
+ }
+ _viewEvents.post(HomeActivityViewEvents.ForceVerification(false))
+ } else {
+ // cross signing keys have been reset
+ // Trigger a popup to re-verify
+ // Note: user can be null in case of logout
+ session.getUser(session.myUserId)
+ ?.toMatrixItem()
+ ?.let { user ->
+ _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user))
+ }
+ }
+ }
+
+ private fun maybeVerifyOrBootstrapCrossSigning() {
+ // The contents of this method should only run once
+ if (hasCheckedBootstrap) return
+ hasCheckedBootstrap = true
+
// We do not use the viewModel context because we do not want to tie this action to activity view model
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) {
- val session = activeSessionHolder.getSafeActiveSession() ?: return@launch
+ val session = activeSessionHolder.getSafeActiveSession() ?: return@launch Unit.also {
+ Timber.w("## No session to init cross signing or bootstrap")
+ }
- tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") {
+ val elementWellKnown = rawService.getElementWellknown(session.sessionParams)
+ val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
+
+ // In case of account creation, it is already done before
+ if (initialState.accountCreation) {
+ if (isSecureBackupRequired) {
+ _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
+ } else {
+ val password = reAuthHelper.data ?: return@launch Unit.also {
+ Timber.w("No password to init cross signing")
+ }
+
+ // Silently initialize cross signing without 4S
+ // We do not use the viewModel context because we do not want to cancel this action
+ Timber.d("Initialize cross signing")
+ try {
+ session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, _ ->
+ resume(
+ UserPasswordAuth(
+ session = response.session,
+ user = session.myUserId,
+ password = password
+ )
+ )
+ }
+ } catch (failure: Throwable) {
+ Timber.e(failure, "Failed to initialize cross signing")
+ }
+ }
+ return@launch
+ }
+
+ tryOrNull("## MaybeVerifyOrBootstrapCrossSigning: Failed to download keys") {
awaitCallback> {
session.cryptoService().downloadKeys(listOf(session.myUserId), true, it)
}
@@ -255,47 +323,68 @@ class HomeActivityViewModel @AssistedInject constructor(
// Is there already cross signing keys here?
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
if (mxCrossSigningInfo != null) {
- // Cross-signing is already set up for this user, is it trusted?
- if (!mxCrossSigningInfo.isTrusted()) {
- // New session
- _viewEvents.post(
- HomeActivityViewEvents.OnNewSession(
- session.getUser(session.myUserId)?.toMatrixItem(),
- // Always send request instead of waiting for an incoming as per recent EW changes
- false
+ if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup()) {
+ // If 4S is forced, start the full interactive setup flow
+ _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
+ } else {
+ // Cross-signing is already set up for this user, is it trusted?
+ if (!mxCrossSigningInfo.isTrusted()) {
+ if (isSecureBackupRequired) {
+ // If 4S is forced, force verification
+ _viewEvents.post(HomeActivityViewEvents.ForceVerification(true))
+ } else {
+ // New session
+ _viewEvents.post(
+ HomeActivityViewEvents.OnNewSession(
+ session.getUser(session.myUserId)?.toMatrixItem(),
+ // Always send request instead of waiting for an incoming as per recent EW changes
+ false
+ )
)
- )
+ }
+ }
}
} else {
- // Try to initialize cross signing in background if possible
- Timber.d("Initialize cross signing...")
- try {
- awaitCallback {
- session.cryptoService().crossSigningService().initializeCrossSigning(
- object : UserInteractiveAuthInterceptor {
- override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) {
- // We missed server grace period or it's not setup, see if we remember locally password
- if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD &&
- errCode == null &&
- reAuthHelper.data != null) {
- promise.resume(
- UserPasswordAuth(
- session = flowResponse.session,
- user = session.myUserId,
- password = reAuthHelper.data
- )
+ // Cross signing is not initialized
+ if (isSecureBackupRequired) {
+ // If 4S is forced, start the full interactive setup flow
+ _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
+ } else {
+ // Initialize cross-signing silently
+ val password = reAuthHelper.data
+
+ if (password == null) {
+ // Check this is not an SSO account
+ if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) {
+ // Ask password to the user: Upgrade security
+ _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
+ }
+ // Else (SSO) just ignore for the moment
+ } else {
+ // Try to initialize cross signing in background if possible
+ Timber.d("Initialize cross signing...")
+ try {
+ session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, errCode ->
+ // We missed server grace period or it's not setup, see if we remember locally password
+ if (response.nextUncompletedStage() == LoginFlowTypes.PASSWORD &&
+ errCode == null &&
+ reAuthHelper.data != null) {
+ resume(
+ UserPasswordAuth(
+ session = response.session,
+ user = session.myUserId,
+ password = reAuthHelper.data
)
- } else {
- promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing"))
- }
- }
- },
- callback = it
- )
- Timber.d("Initialize cross signing SUCCESS")
+ )
+ Timber.d("Initialize cross signing SUCCESS")
+ } else {
+ resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing"))
+ }
+ }
+ } catch (failure: Throwable) {
+ Timber.e(failure, "Failed to initialize cross signing")
+ }
}
- } catch (failure: Throwable) {
- Timber.e(failure, "Failed to initialize cross signing")
}
}
}
@@ -306,9 +395,24 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.PushPromptHasBeenReviewed -> {
vectorPreferences.setDidAskUserToEnableSessionPush()
}
- HomeActivityViewActions.ViewStarted -> {
+ HomeActivityViewActions.ViewStarted -> {
initialize()
}
}
}
}
+
+private suspend fun CrossSigningService.awaitCrossSigninInitialization(
+ block: Continuation.(response: RegistrationFlowResponse, errCode: String?) -> Unit
+) {
+ awaitCallback {
+ initializeCrossSigning(
+ object : UserInteractiveAuthInterceptor {
+ override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) {
+ promise.block(flowResponse, errCode)
+ }
+ },
+ callback = it
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
index 68131e8569..45fe04fc61 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
@@ -20,5 +20,6 @@ import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
data class HomeActivityViewState(
- val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle
+ val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle,
+ val accountCreation: Boolean = false
) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index 0ed6cc8d9d..4eedb528d1 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -33,6 +33,7 @@ import im.vector.app.R
import im.vector.app.RoomGroupingMethod
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.toMvRxBundle
+import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@@ -69,7 +70,8 @@ class HomeDetailFragment @Inject constructor(
private val appStateHandler: AppStateHandler
) : VectorBaseFragment(),
KeysBackupBanner.Delegate,
- CurrentCallsView.Callback {
+ CurrentCallsView.Callback,
+ OnBackPressed {
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
@@ -130,12 +132,8 @@ class HomeDetailFragment @Inject constructor(
viewModel.onEach(HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod ->
when (roomGroupingMethod) {
- is RoomGroupingMethod.ByLegacyGroup -> {
- onGroupChange(roomGroupingMethod.groupSummary)
- }
- is RoomGroupingMethod.BySpace -> {
- onSpaceChange(roomGroupingMethod.spaceSummary)
- }
+ is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
+ is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
}
}
@@ -149,15 +147,14 @@ class HomeDetailFragment @Inject constructor(
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
- HomeDetailViewEvents.CallStarted -> handleCallStarted()
+ HomeDetailViewEvents.CallStarted -> handleCallStarted()
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
- HomeDetailViewEvents.Loading -> showLoadingDialog()
+ HomeDetailViewEvents.Loading -> showLoadingDialog()
}
}
unknownDeviceDetectorSharedViewModel.onEach { state ->
state.unknownSessions.invoke()?.let { unknownDevices ->
-// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
val uid = "review_login"
alertManager.cancelAlert(uid)
@@ -190,6 +187,27 @@ class HomeDetailFragment @Inject constructor(
}
}
+ private fun navigateBack() {
+ try {
+ val lastSpace = appStateHandler.getSpaceBackstack().removeLast()
+ setCurrentSpace(lastSpace)
+ } catch (e: NoSuchElementException) {
+ navigateUpOneSpace()
+ }
+ }
+
+ private fun setCurrentSpace(spaceId: String?) {
+ appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
+ sharedActionViewModel.post(HomeActivitySharedAction.CloseGroup)
+ }
+
+ private fun navigateUpOneSpace() {
+ val parentId = getCurrentSpace()?.flattenParentIds?.lastOrNull()
+ setCurrentSpace(parentId)
+ }
+
+ private fun getCurrentSpace() = (appStateHandler.getCurrentRoomGroupingMethod() as? RoomGroupingMethod.BySpace)?.spaceSummary
+
private fun handleCallStarted() {
dismissLoadingDialog()
val fragmentTag = HomeTab.DialPad.toFragmentTag()
@@ -203,20 +221,16 @@ class HomeDetailFragment @Inject constructor(
override fun onResume() {
super.onResume()
- // update notification tab if needed
updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
callManager.checkForProtocolsSupportIfNeeded()
+ refreshSpaceState()
+ }
- // Current space/group is not live so at least refresh toolbar on resume
- appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod ->
- when (roomGroupingMethod) {
- is RoomGroupingMethod.ByLegacyGroup -> {
- onGroupChange(roomGroupingMethod.groupSummary)
- }
- is RoomGroupingMethod.BySpace -> {
- onSpaceChange(roomGroupingMethod.spaceSummary)
- }
- }
+ private fun refreshSpaceState() {
+ when (val roomGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) {
+ is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
+ is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
+ else -> Unit
}
}
@@ -260,12 +274,12 @@ class HomeDetailFragment @Inject constructor(
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
contentAction = Runnable {
- (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
+ (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity ->
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
- it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
+ activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
@@ -299,10 +313,10 @@ class HomeDetailFragment @Inject constructor(
serverBackupStatusViewModel
.onEach {
when (val banState = it.bannerState.invoke()) {
- is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
+ is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
null,
- BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
+ BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
}
views.homeKeysBackupBanner.delegate = this
@@ -324,11 +338,11 @@ class HomeDetailFragment @Inject constructor(
withState(viewModel) {
when (it.roomGroupingMethod) {
is RoomGroupingMethod.ByLegacyGroup -> {
- // nothing do far
+ // do nothing
}
- is RoomGroupingMethod.BySpace -> {
- it.roomGroupingMethod.spaceSummary?.let {
- sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(it.roomId))
+ is RoomGroupingMethod.BySpace -> {
+ it.roomGroupingMethod.spaceSummary?.let { spaceSummary ->
+ sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
}
}
}
@@ -340,25 +354,14 @@ class HomeDetailFragment @Inject constructor(
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
views.bottomNavigationView.setOnItemSelectedListener {
val tab = when (it.itemId) {
- R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
- R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
+ R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
+ R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS)
- else -> HomeTab.DialPad
+ else -> HomeTab.DialPad
}
viewModel.handle(HomeDetailAction.SwitchTab(tab))
true
}
-
-// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
-
-// bottomNavigationView.getOrCreateBadge()
-// menuView.forEachIndexed { index, view ->
-// val itemView = view as BottomNavigationItemView
-// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
-// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
-// itemView.addView(badgeLayout)
-// unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
-// }
}
private fun updateUIForTab(tab: HomeTab) {
@@ -385,7 +388,7 @@ class HomeDetailFragment @Inject constructor(
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
- is HomeTab.DialPad -> {
+ is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
}
}
@@ -436,7 +439,6 @@ class HomeDetailFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) {
-// Timber.v(it.toString())
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
@@ -463,11 +465,11 @@ class HomeDetailFragment @Inject constructor(
}
private fun HomeTab.toMenuId() = when (this) {
- is HomeTab.DialPad -> R.id.bottom_action_dial_pad
+ is HomeTab.DialPad -> R.id.bottom_action_dial_pad
is HomeTab.RoomList -> when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
- RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
- else -> R.id.bottom_action_notification
+ RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
+ else -> R.id.bottom_action_notification
}
}
@@ -496,4 +498,11 @@ class HomeDetailFragment @Inject constructor(
}
return this
}
+
+ override fun onBackPressed(toolbarButton: Boolean) = if (getCurrentSpace() != null) {
+ navigateBack()
+ true
+ } else {
+ false
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
index ced911ee42..820ce2ef49 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
@@ -45,8 +45,9 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter
+import org.matrix.android.sdk.api.query.SpaceFilter
+import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
@@ -241,7 +242,7 @@ class HomeDetailViewModel @AssistedInject constructor(
roomSummaryQueryParams {
memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_DM
- activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
+ spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) }
}
).size
@@ -249,7 +250,7 @@ class HomeDetailViewModel @AssistedInject constructor(
roomSummaryQueryParams {
memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
+ spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms()
}
).size
}
@@ -258,7 +259,7 @@ class HomeDetailViewModel @AssistedInject constructor(
roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
roomCategoryFilter = RoomCategoryFilter.ONLY_DM
- activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None
+ spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) }
}
)
@@ -266,7 +267,7 @@ class HomeDetailViewModel @AssistedInject constructor(
roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId)
+ spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms()
}
)
@@ -287,4 +288,8 @@ class HomeDetailViewModel @AssistedInject constructor(
}
.launchIn(viewModelScope)
}
+
+ private fun RoomGroupingMethod.BySpace.toActiveSpaceOrOrphanRooms(): SpaceFilter? {
+ return spaceSummary?.roomId?.toActiveSpaceOrOrphanRooms()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt
index 6b70ac4560..723ca7caae 100644
--- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt
@@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import org.matrix.android.sdk.api.query.ActiveSpaceFilter
+import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.model.Membership
@@ -76,11 +76,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
private val roomService = session.roomService()
init {
-
roomService.getPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
- this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
+ this.spaceFilter = SpaceFilter.OrphanRooms
}, sortOrder = RoomSortOrder.NONE
).asFlow()
.throttleFirst(300)
@@ -88,7 +87,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
val counts = roomService.getNotificationCountForRooms(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
- this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
+ this.spaceFilter = SpaceFilter.OrphanRooms
}
)
val invites = if (autoAcceptInvites.hideInvites) {
@@ -97,7 +96,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
roomService.getRoomSummaries(
roomSummaryQueryParams {
this.memberships = listOf(Membership.INVITE)
- this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
+ this.spaceFilter = SpaceFilter.OrphanRooms
}
).size
}
@@ -153,9 +152,9 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
val totalCount = roomService.getNotificationCountForRooms(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
- this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
+ this.spaceFilter = SpaceFilter.OrphanRooms.takeIf {
!vectorPreferences.prefSpacesShowAllRoomInHome()
- } ?: ActiveSpaceFilter.None
+ }
}
)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index b98d3eced2..1f8b922090 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -74,6 +74,9 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.containsRtLOverride
+import im.vector.app.core.extensions.ensureEndsLeftToRight
+import im.vector.app.core.extensions.filterDirectionOverrides
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextOrHide
@@ -221,6 +224,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
@@ -417,10 +421,10 @@ class TimelineFragment @Inject constructor(
}
when (mode) {
is SendMode.Regular -> renderRegularMode(mode.text)
- is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
- is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.action_quote, mode.text)
- is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
- is SendMode.Voice -> renderVoiceMessageMode(mode.text)
+ is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
+ is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.action_quote, mode.text)
+ is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
+ is SendMode.Voice -> renderVoiceMessageMode(mode.text)
}
}
@@ -439,13 +443,13 @@ class TimelineFragment @Inject constructor(
messageComposerViewModel.observeViewEvents {
when (it) {
- is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
+ is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is MessageComposerViewEvents.SlashCommandConfirmationRequest -> handleSlashCommandConfirmationRequest(it)
- is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
- is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
- is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
- is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
- is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
+ is MessageComposerViewEvents.SendMessageResult -> renderSendMessageResult(it)
+ is MessageComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message)
+ is MessageComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it)
+ is MessageComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it)
+ is MessageComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId)
is MessageComposerViewEvents.VoicePlaybackOrRecordingFailure -> {
if (it.throwable is VoiceFailure.UnableToRecord) {
onCannotRecord()
@@ -457,39 +461,39 @@ class TimelineFragment @Inject constructor(
timelineViewModel.observeViewEvents {
when (it) {
- is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
- is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
- is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
- is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
- is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message)
- is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
- is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
- is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
- RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
- is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
+ is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
+ is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
+ is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
+ is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
+ is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message)
+ is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
+ is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
+ is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
+ RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
+ is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
- is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
- is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it)
- RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
- is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
- is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
- RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
- RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
- RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
- is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
- is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
- RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), timelineArgs.roomId)
- RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
- RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings(RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS)
- RoomDetailViewEvents.OpenRoomProfile -> handleOpenRoomSettings()
- is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
+ is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
+ is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it)
+ RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
+ is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
+ is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
+ RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
+ RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
+ RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
+ is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
+ is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
+ RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), timelineArgs.roomId)
+ RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
+ RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings(RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS)
+ RoomDetailViewEvents.OpenRoomProfile -> handleOpenRoomSettings()
+ is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item)
}
- is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type)
- RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
- is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
- RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
- is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it)
+ is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type)
+ RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
+ is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
+ RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
+ is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it)
}
}
@@ -502,7 +506,7 @@ class TimelineFragment @Inject constructor(
private fun handleSlashCommandConfirmationRequest(action: MessageComposerViewEvents.SlashCommandConfirmationRequest) {
when (action.parsedCommand) {
is ParsedCommand.UnignoreUser -> promptUnignoreUser(action.parsedCommand)
- else -> TODO("Add case for ${action.parsedCommand.javaClass.simpleName}")
+ else -> TODO("Add case for ${action.parsedCommand.javaClass.simpleName}")
}
lockSendButton = false
}
@@ -647,6 +651,13 @@ class TimelineFragment @Inject constructor(
)
}
+ private fun navigateToLocationLiveMap() {
+ navigator.openLocationLiveMap(
+ context = requireContext(),
+ roomId = timelineArgs.roomId
+ )
+ }
+
private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) {
views.locationLiveStatusIndicator.isVisible = event.isVisible
}
@@ -706,31 +717,31 @@ class TimelineFragment @Inject constructor(
}
private fun createEmojiPopup(): EmojiPopup {
- return EmojiPopup
- .Builder
- .fromRootView(views.rootConstraintLayout)
- .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
- .setOnEmojiPopupShownListener {
+ return EmojiPopup(
+ rootView = views.rootConstraintLayout,
+ keyboardAnimationStyle = R.style.emoji_fade_animation_style,
+ onEmojiPopupShownListener = {
views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_close_emoji_picker)
setImageResource(R.drawable.ic_keyboard)
}
- }
- .setOnEmojiPopupDismissListenerLifecycleAware {
+ },
+ onEmojiPopupDismissListener = lifecycleAwareDismissAction {
views.composerLayout.views.composerEmojiButton.apply {
contentDescription = getString(R.string.a11y_open_emoji_picker)
setImageResource(R.drawable.ic_insert_emoji)
}
- }
- .build(views.composerLayout.views.composerEditText)
+ },
+ editText = views.composerLayout.views.composerEditText
+ )
}
/**
* Ensure dismiss actions only trigger when the fragment is in the started state.
* EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView.
*/
- private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder {
- return setOnEmojiPopupDismissListener {
+ private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit {
+ return {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
action()
}
@@ -926,14 +937,14 @@ class TimelineFragment @Inject constructor(
private fun handleShareData() {
when (val sharedData = timelineArgs.sharedData) {
- is SharedData.Text -> {
+ is SharedData.Text -> {
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(sharedData.text, fromSharing = true))
}
is SharedData.Attachments -> {
// open share edition
onContentAttachmentsReady(sharedData.attachmentData)
}
- null -> Timber.v("No share data to process")
+ null -> Timber.v("No share data to process")
}
}
@@ -1076,8 +1087,8 @@ class TimelineFragment @Inject constructor(
// Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions
val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined
val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
- 1 -> false
- 2 -> state.isAllowedToStartWebRTCCall
+ 1 -> false
+ 2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets
}
setOf(R.id.voice_call, R.id.video_call).forEach {
@@ -1111,39 +1122,39 @@ class TimelineFragment @Inject constructor(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
- R.id.invite -> {
+ R.id.invite -> {
navigator.openInviteUsersToRoom(requireActivity(), timelineArgs.roomId)
true
}
- R.id.timeline_setting -> {
+ R.id.timeline_setting -> {
navigator.openRoomProfile(requireActivity(), timelineArgs.roomId)
true
}
- R.id.open_matrix_apps -> {
+ R.id.open_matrix_apps -> {
timelineViewModel.handle(RoomDetailAction.ManageIntegrations)
true
}
- R.id.voice_call -> {
+ R.id.voice_call -> {
callActionsHandler.onVoiceCallClicked()
true
}
- R.id.video_call -> {
+ R.id.video_call -> {
callActionsHandler.onVideoCallClicked()
true
}
- R.id.menu_timeline_thread_list -> {
+ R.id.menu_timeline_thread_list -> {
navigateToThreadList()
true
}
- R.id.search -> {
+ R.id.search -> {
handleSearchAction()
true
}
- R.id.dev_tools -> {
+ R.id.dev_tools -> {
navigator.openDevTools(requireContext(), timelineArgs.roomId)
true
}
- R.id.menu_thread_timeline_copy_link -> {
+ R.id.menu_thread_timeline_copy_link -> {
getRootThreadEventId()?.let {
val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, it)
copyToClipboard(requireContext(), permalink, false)
@@ -1155,14 +1166,14 @@ class TimelineFragment @Inject constructor(
handleViewInRoomAction()
true
}
- R.id.menu_thread_timeline_share -> {
+ R.id.menu_thread_timeline_share -> {
getRootThreadEventId()?.let {
val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, it)
shareText(requireContext(), permalink)
}
true
}
- else -> super.onOptionsItemSelected(item)
+ else -> super.onOptionsItemSelected(item)
}
}
@@ -1246,8 +1257,8 @@ class TimelineFragment @Inject constructor(
val messageContent: MessageContent? = event.getLastMessageContent()
val nonFormattedBody = when (messageContent) {
is MessageAudioContent -> getAudioContentBodyText(messageContent)
- is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
- else -> messageContent?.body.orEmpty()
+ is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
+ else -> messageContent?.body.orEmpty()
}
var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
@@ -1309,9 +1320,9 @@ class TimelineFragment @Inject constructor(
when (roomDetailPendingAction) {
is RoomDetailPendingAction.JumpToReadReceipt ->
timelineViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId))
- is RoomDetailPendingAction.MentionUser ->
+ is RoomDetailPendingAction.MentionUser ->
insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
- is RoomDetailPendingAction.OpenRoom ->
+ is RoomDetailPendingAction.OpenRoom ->
handleOpenRoom(RoomDetailViewEvents.OpenRoom(roomDetailPendingAction.roomId, roomDetailPendingAction.closeCurrentRoom))
}
}
@@ -1453,7 +1464,7 @@ class TimelineFragment @Inject constructor(
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
}
- else -> false
+ else -> false
}
}
}
@@ -1479,9 +1490,9 @@ class TimelineFragment @Inject constructor(
val state = timelineViewModel.awaitState()
val showJumpToUnreadBanner = when (state.unreadState) {
UnreadState.Unknown,
- UnreadState.HasNoUnread -> false
+ UnreadState.HasNoUnread -> false
is UnreadState.ReadMarkerNotLoaded -> true
- is UnreadState.HasUnread -> {
+ is UnreadState.HasUnread -> {
if (state.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
val positionOfReadMarker = withContext(Dispatchers.Default) {
@@ -1636,10 +1647,10 @@ class TimelineFragment @Inject constructor(
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
// views.composerLayout.alwaysShowSendButton = false
when (messageComposerState.canSendMessage) {
- CanSendStatus.Allowed -> {
+ CanSendStatus.Allowed -> {
NotificationAreaView.State.Hidden
}
- CanSendStatus.NoPermission -> {
+ CanSendStatus.NoPermission -> {
NotificationAreaView.State.NoPermissionToPost
}
is CanSendStatus.UnSupportedE2eAlgorithm -> {
@@ -1713,23 +1724,23 @@ class TimelineFragment @Inject constructor(
private fun renderSendMessageResult(sendMessageResult: MessageComposerViewEvents.SendMessageResult) {
when (sendMessageResult) {
- is MessageComposerViewEvents.SlashCommandLoading -> {
+ is MessageComposerViewEvents.SlashCommandLoading -> {
showLoading(null)
}
- is MessageComposerViewEvents.SlashCommandError -> {
+ is MessageComposerViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
- is MessageComposerViewEvents.SlashCommandUnknown -> {
+ is MessageComposerViewEvents.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
- is MessageComposerViewEvents.SlashCommandResultOk -> {
+ is MessageComposerViewEvents.SlashCommandResultOk -> {
handleSlashCommandResultOk(sendMessageResult.parsedCommand)
}
- is MessageComposerViewEvents.SlashCommandResultError -> {
+ is MessageComposerViewEvents.SlashCommandResultError -> {
dismissLoadingDialog()
displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
}
- is MessageComposerViewEvents.SlashCommandNotImplemented -> {
+ is MessageComposerViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
}
is MessageComposerViewEvents.SlashCommandNotSupportedInThreads -> {
@@ -1747,7 +1758,7 @@ class TimelineFragment @Inject constructor(
is ParsedCommand.SetMarkdown -> {
showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
}
- else -> Unit
+ else -> Unit
}
}
@@ -1762,10 +1773,10 @@ class TimelineFragment @Inject constructor(
private fun displayE2eError(withHeldCode: WithHeldCode?) {
val msgId = when (withHeldCode) {
WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted
- WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
+ WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
WithHeldCode.UNAUTHORISED,
WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic
- else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc
+ else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc
}
MaterialAlertDialogBuilder(requireActivity())
.setMessage(msgId)
@@ -1813,9 +1824,9 @@ class TimelineFragment @Inject constructor(
private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) {
when (val data = result.action) {
- is RoomDetailAction.ReportContent -> {
+ is RoomDetailAction.ReportContent -> {
when {
- data.spam -> {
+ data.spam -> {
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
@@ -1835,7 +1846,7 @@ class TimelineFragment @Inject constructor(
}
.show()
}
- else -> {
+ else -> {
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
@@ -1847,7 +1858,7 @@ class TimelineFragment @Inject constructor(
}
}
}
- is RoomDetailAction.RequestVerification -> {
+ is RoomDetailAction.RequestVerification -> {
Timber.v("## SAS RequestVerification action")
VerificationBottomSheet.withArgs(
timelineArgs.roomId,
@@ -1862,7 +1873,7 @@ class TimelineFragment @Inject constructor(
data.transactionId
).show(parentFragmentManager, "REQ")
}
- is RoomDetailAction.ResumeVerification -> {
+ is RoomDetailAction.ResumeVerification -> {
val otherUserId = data.otherUserId ?: return
VerificationBottomSheet.withArgs(
roomId = timelineArgs.roomId,
@@ -1870,7 +1881,7 @@ class TimelineFragment @Inject constructor(
transactionId = data.transactionId,
).show(parentFragmentManager, "REQ")
}
- else -> Unit
+ else -> Unit
}
}
@@ -1916,23 +1927,20 @@ class TimelineFragment @Inject constructor(
}
})
if (!isManaged) {
- if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) {
- MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
- .setTitle(R.string.external_link_confirmation_title)
- .setMessage(
- getString(R.string.external_link_confirmation_message, title, url)
- .toSpannable()
- .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
- .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
- )
- .setPositiveButton(R.string._continue) { _, _ ->
- openUrlInExternalBrowser(requireContext(), url)
- }
- .setNegativeButton(R.string.action_cancel, null)
- .show()
- } else {
- // Open in external browser, in a new Tab
- openUrlInExternalBrowser(requireContext(), url)
+ when {
+ url.containsRtLOverride() -> {
+ displayUrlConfirmationDialog(
+ seenUrl = title.ensureEndsLeftToRight(),
+ actualUrl = url.filterDirectionOverrides(),
+ continueTo = url
+ )
+ }
+ title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host -> {
+ displayUrlConfirmationDialog(title, url)
+ }
+ else -> {
+ openUrlInExternalBrowser(requireContext(), url)
+ }
}
}
}
@@ -1940,6 +1948,22 @@ class TimelineFragment @Inject constructor(
return true
}
+ private fun displayUrlConfirmationDialog(seenUrl: String, actualUrl: String, continueTo: String = actualUrl) {
+ MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
+ .setTitle(R.string.external_link_confirmation_title)
+ .setMessage(
+ getString(R.string.external_link_confirmation_message, seenUrl, actualUrl)
+ .toSpannable()
+ .colorizeMatchingText(actualUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
+ .colorizeMatchingText(seenUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary))
+ )
+ .setPositiveButton(R.string._continue) { _, _ ->
+ openUrlInExternalBrowser(requireContext(), continueTo)
+ }
+ .setNegativeButton(R.string.action_cancel, null)
+ .show()
+ }
+
override fun onUrlLongClicked(url: String): Boolean {
if (url != getString(R.string.edited_suffix) && url.isValidUrl()) {
// Copy the url to the clipboard
@@ -2009,17 +2033,20 @@ class TimelineFragment @Inject constructor(
is MessageVerificationRequestContent -> {
timelineViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
}
- is MessageWithAttachmentContent -> {
+ is MessageWithAttachmentContent -> {
val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, informationData.senderId, messageContent)
timelineViewModel.handle(action)
}
- is EncryptedEventContent -> {
+ is EncryptedEventContent -> {
timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
}
- is MessageLocationContent -> {
+ is MessageLocationContent -> {
handleShowLocationPreview(messageContent, informationData.senderId)
}
- else -> {
+ is MessageBeaconInfoContent -> {
+ navigateToLocationLiveMap()
+ }
+ else -> {
val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
if (!handled) {
Timber.d("No click action defined for this message content")
@@ -2156,8 +2183,8 @@ class TimelineFragment @Inject constructor(
private fun onShareActionClicked(action: EventSharedAction.Share) {
when (action.messageContent) {
- is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
- is MessageLocationContent -> {
+ is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
+ is MessageLocationContent -> {
action.messageContent.toLocationData()?.let {
openLocation(requireActivity(), it.latitude, it.longitude)
}
@@ -2217,52 +2244,52 @@ class TimelineFragment @Inject constructor(
private fun handleActions(action: EventSharedAction) {
when (action) {
- is EventSharedAction.OpenUserProfile -> {
+ is EventSharedAction.OpenUserProfile -> {
openRoomMemberProfile(action.userId)
}
- is EventSharedAction.AddReaction -> {
+ is EventSharedAction.AddReaction -> {
openEmojiReactionPicker(action.eventId)
}
- is EventSharedAction.ViewReactions -> {
+ is EventSharedAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
- is EventSharedAction.Copy -> {
+ is EventSharedAction.Copy -> {
// I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false)
showSnackWithMessage(getString(R.string.copied_to_clipboard))
}
- is EventSharedAction.Redact -> {
+ is EventSharedAction.Redact -> {
promptConfirmationToRedactEvent(action)
}
- is EventSharedAction.Share -> {
+ is EventSharedAction.Share -> {
onShareActionClicked(action)
}
- is EventSharedAction.Save -> {
+ is EventSharedAction.Save -> {
onSaveActionClicked(action)
}
- is EventSharedAction.ViewEditHistory -> {
+ is EventSharedAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData)
}
- is EventSharedAction.ViewSource -> {
+ is EventSharedAction.ViewSource -> {
JSonViewerDialog.newInstance(
action.content,
-1,
createJSonViewerStyleProvider(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
}
- is EventSharedAction.ViewDecryptedSource -> {
+ is EventSharedAction.ViewDecryptedSource -> {
JSonViewerDialog.newInstance(
action.content,
-1,
createJSonViewerStyleProvider(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
}
- is EventSharedAction.QuickReact -> {
+ is EventSharedAction.QuickReact -> {
// eventId,ClickedOn,Add
timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
- is EventSharedAction.Edit -> {
+ is EventSharedAction.Edit -> {
if (action.eventType in EventType.POLL_START) {
navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT)
} else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
@@ -2271,45 +2298,45 @@ class TimelineFragment @Inject constructor(
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
- is EventSharedAction.Quote -> {
+ is EventSharedAction.Quote -> {
messageComposerViewModel.handle(MessageComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
}
- is EventSharedAction.Reply -> {
+ is EventSharedAction.Reply -> {
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
messageComposerViewModel.handle(MessageComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
} else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
- is EventSharedAction.ReplyInThread -> {
+ is EventSharedAction.ReplyInThread -> {
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
onReplyInThreadClicked(action)
} else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
- is EventSharedAction.ViewInRoom -> {
+ is EventSharedAction.ViewInRoom -> {
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
handleViewInRoomAction()
} else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
- is EventSharedAction.CopyPermalink -> {
+ is EventSharedAction.CopyPermalink -> {
val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(getString(R.string.copied_to_clipboard))
}
- is EventSharedAction.Resend -> {
+ is EventSharedAction.Resend -> {
timelineViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
}
- is EventSharedAction.Remove -> {
+ is EventSharedAction.Remove -> {
timelineViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
}
- is EventSharedAction.Cancel -> {
+ is EventSharedAction.Cancel -> {
handleCancelSend(action)
}
- is EventSharedAction.ReportContentSpam -> {
+ is EventSharedAction.ReportContentSpam -> {
timelineViewModel.handle(
RoomDetailAction.ReportContent(
action.eventId, action.senderId, "This message is spam", spam = true
@@ -2323,31 +2350,31 @@ class TimelineFragment @Inject constructor(
)
)
}
- is EventSharedAction.ReportContentCustom -> {
+ is EventSharedAction.ReportContentCustom -> {
promptReasonToReportContent(action)
}
- is EventSharedAction.IgnoreUser -> {
+ is EventSharedAction.IgnoreUser -> {
action.senderId?.let { askConfirmationToIgnoreUser(it) }
}
- is EventSharedAction.OnUrlClicked -> {
+ is EventSharedAction.OnUrlClicked -> {
onUrlClicked(action.url, action.title)
}
- is EventSharedAction.OnUrlLongClicked -> {
+ is EventSharedAction.OnUrlLongClicked -> {
onUrlLongClicked(action.url)
}
- is EventSharedAction.ReRequestKey -> {
+ is EventSharedAction.ReRequestKey -> {
timelineViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId))
}
- is EventSharedAction.UseKeyBackup -> {
+ is EventSharedAction.UseKeyBackup -> {
context?.let {
startActivity(KeysBackupRestoreActivity.intent(it))
}
}
- is EventSharedAction.EndPoll -> {
+ is EventSharedAction.EndPoll -> {
askConfirmationToEndPoll(action.eventId)
}
- is EventSharedAction.ReportContent -> Unit /* Not clickable */
- EventSharedAction.Separator -> Unit /* Not clickable */
+ is EventSharedAction.ReportContent -> Unit /* Not clickable */
+ EventSharedAction.Separator -> Unit /* Not clickable */
}
}
@@ -2557,17 +2584,17 @@ class TimelineFragment @Inject constructor(
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) {
- AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
+ AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
activity = requireActivity(),
vectorPreferences = vectorPreferences,
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
)
- AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
- AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
- AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
- AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
- AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId, null, PollMode.CREATE)
+ AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+ AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId, null, PollMode.CREATE)
AttachmentTypeSelectorView.Type.LOCATION -> {
navigator
.openLocationSharing(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 214d01cb98..1b630801cc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -211,7 +211,8 @@ class TimelineViewModel @AssistedInject constructor(
appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let
// nothing we are good
- if (currentSpace == null || !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId)) {
+ if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) ||
+ (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) {
// take first one or switch to home
appStateHandler.setCurrentSpace(
currentRoomSummary
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
index 0be462835d..7bf9f536f2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
@@ -54,7 +54,7 @@ sealed class EventSharedAction(
EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread)
object ViewInRoom :
- EventSharedAction(R.string.view_in_room, R.drawable.ic_thread_view_in_room_menu_item)
+ EventSharedAction(R.string.view_in_room, R.drawable.ic_threads_view_in_room_24)
data class Share(val eventId: String, val messageContent: MessageContent) :
EventSharedAction(R.string.action_share, R.drawable.ic_share)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 47f26d3fb1..15d8eae509 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -405,8 +405,8 @@ class MessageActionsViewModel @AssistedInject constructor(
if (vectorPreferences.developerMode()) {
if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) {
val keysBackupService = session.cryptoService().keysBackupService()
- if (keysBackupService.state == KeysBackupState.NotTrusted ||
- (keysBackupService.state == KeysBackupState.ReadyToBackUp &&
+ if (keysBackupService.getState() == KeysBackupState.NotTrusted ||
+ (keysBackupService.getState() == KeysBackupState.ReadyToBackUp &&
keysBackupService.canRestoreKeys())
) {
add(EventSharedAction.UseKeyBackup)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
index 8629a3a47b..3c7b6c32e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
@@ -33,7 +33,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocation
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData
-import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime
@@ -57,10 +56,10 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
): VectorEpoxyModel<*>? {
val liveLocationShareSummaryData = getLiveLocationShareSummaryData(event)
val item = when (val currentState = getViewState(liveLocationShareSummaryData)) {
- LiveLocationShareViewState.Inactive -> buildInactiveItem(highlight, attributes)
- LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes)
+ LiveLocationShareViewState.Inactive -> buildInactiveItem(highlight, attributes)
+ LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes)
is LiveLocationShareViewState.Running -> buildRunningItem(highlight, attributes, currentState)
- LiveLocationShareViewState.Unkwown -> null
+ LiveLocationShareViewState.Unkwown -> null
}
item?.layout(attributes.informationData.messageLayout.layoutRes)
@@ -75,7 +74,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationInactiveItem_()
- .attributes(attributes)
+ // disable the click on this state item
+ .attributes(attributes.copy(itemClickListener = null))
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight)
@@ -90,7 +90,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
return MessageLiveLocationStartItem_()
- .attributes(attributes)
+ // disable the click on this state item
+ .attributes(attributes.copy(itemClickListener = null))
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight)
@@ -126,10 +127,10 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState {
return when {
- liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown
- liveLocationShareSummaryData.isActive.not() || isLiveTimedOut(liveLocationShareSummaryData) -> LiveLocationShareViewState.Inactive
+ liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown
+ liveLocationShareSummaryData.isActive.not() -> LiveLocationShareViewState.Inactive
liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading
- else ->
+ else ->
LiveLocationShareViewState.Running(
liveLocationShareSummaryData.lastGeoUri.orEmpty(),
getEndOfLiveDateTime(liveLocationShareSummaryData)
@@ -137,16 +138,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
}.also { viewState -> Timber.d("computed viewState: $viewState") }
}
- private fun isLiveTimedOut(liveLocationShareSummaryData: LiveLocationShareSummaryData): Boolean {
- return getEndOfLiveDateTime(liveLocationShareSummaryData)
- ?.let { endOfLive ->
- // this will only cover users with different timezones but not users with manually time set
- val now = LocalDateTime.now()
- now.isAfter(endOfLive)
- }
- .orFalse()
- }
-
private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) }
}
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 bcbbfb9ebd..e86d3b2ec6 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
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.getMsgType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
@@ -121,6 +122,7 @@ class MessageInformationDataFactory @Inject constructor(
isLastFromThisSender = isLastFromThisSender,
e2eDecoration = e2eDecoration,
sendStateDecoration = sendStateDecoration,
+ messageType = event.root.getMsgType()
)
}
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 d005135032..ad7482d9a5 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
@@ -70,7 +70,8 @@ class MessageItemAttributesFactory @Inject constructor(
threadSummaryFormatted = displayableEventFormatter.formatThreadSummary(threadDetails?.threadSummaryLatestEvent).toString(),
threadDetails = threadDetails,
reactionsSummaryEvents = reactionsSummaryEvents,
- areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled()
+ areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled(),
+ autoplayAnimatedImages = preferencesProvider.autoplayAnimatedImages()
)
}
}
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 b9d79d5818..24a148885f 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
@@ -188,6 +188,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
val threadSummaryFormatted: String? = null,
val threadDetails: ThreadDetails? = null,
val areThreadMessagesEnabled: Boolean = false,
+ val autoplayAnimatedImages: Boolean = false,
override val reactionsSummaryEvents: ReactionsSummaryEvents? = null,
) : AbsBaseMessageItem.Attributes {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
index 8485c40ef9..f3ab0e838b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
@@ -34,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
import im.vector.app.features.media.ImageContentRenderer
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageImageVideoItem : AbsMessageItem() {
@@ -80,7 +81,17 @@ abstract class MessageImageVideoItem : AbsMessageItem :
}
override fun buildModels() {
- check(isBuildingModels()) {
+ check(isBuildingModels) {
("You cannot call `buildModels` directly. Call `setData` instead to trigger a model " +
"refresh with new data.")
}
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 2949425004..674f5022f2 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
@@ -110,8 +110,8 @@ class RoomListFragment @Inject constructor(
super.onCreate(savedInstanceState)
analyticsScreenName = when (roomListParams.displayMode) {
RoomListDisplayMode.PEOPLE -> MobileScreen.ScreenName.People
- RoomListDisplayMode.ROOMS -> MobileScreen.ScreenName.Rooms
- else -> null
+ RoomListDisplayMode.ROOMS -> MobileScreen.ScreenName.Rooms
+ else -> null
}
}
@@ -124,10 +124,10 @@ class RoomListFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
roomListViewModel.observeViewEvents {
when (it) {
- is RoomListViewEvents.Loading -> showLoading(it.message)
- is RoomListViewEvents.Failure -> showFailure(it.throwable)
- is RoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
- is RoomListViewEvents.Done -> Unit
+ is RoomListViewEvents.Loading -> showLoading(it.message)
+ is RoomListViewEvents.Failure -> showFailure(it.throwable)
+ is RoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
+ is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
}
}
@@ -209,9 +209,9 @@ class RoomListFragment @Inject constructor(
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.isVisible = true
- RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true
- RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true
- else -> Unit // No button in this mode
+ RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true
+ RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true
+ RoomListDisplayMode.FILTERED -> Unit // No button in this mode
}
views.createChatRoomButton.debouncedClicks {
@@ -228,16 +228,16 @@ class RoomListFragment @Inject constructor(
views.createChatFabMenu.removeCallbacks(showFabRunnable)
when (newState) {
- RecyclerView.SCROLL_STATE_IDLE -> {
+ RecyclerView.SCROLL_STATE_IDLE -> {
views.createChatFabMenu.postDelayed(showFabRunnable, 250)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.hide()
- RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.hide()
- RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.hide()
- else -> Unit
+ RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.hide()
+ RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.hide()
+ RoomListDisplayMode.FILTERED -> Unit
}
}
}
@@ -293,8 +293,8 @@ class RoomListFragment @Inject constructor(
}
val contentAdapter =
when {
- section.livePages != null -> {
- pagedControllerFactory.createRoomSummaryPagedController()
+ section.livePages != null -> {
+ pagedControllerFactory.createRoomSummaryPagedController(roomListParams.displayMode)
.also { controller ->
section.livePages.observe(viewLifecycleOwner) { pl ->
controller.submitList(pl)
@@ -316,7 +316,7 @@ class RoomListFragment @Inject constructor(
)
}
}
- section.isExpanded.observe(viewLifecycleOwner) { _ ->
+ section.isExpanded.observe(viewLifecycleOwner) {
refreshCollapseStates()
}
controller.listener = this
@@ -337,14 +337,14 @@ class RoomListFragment @Inject constructor(
checkEmptyState()
}
observeItemCount(section, sectionAdapter)
- section.isExpanded.observe(viewLifecycleOwner) { _ ->
+ section.isExpanded.observe(viewLifecycleOwner) {
refreshCollapseStates()
}
controller.listener = this
}
}
- else -> {
- pagedControllerFactory.createRoomSummaryListController()
+ else -> {
+ pagedControllerFactory.createRoomSummaryListController(roomListParams.displayMode)
.also { controller ->
section.liveList?.observe(viewLifecycleOwner) { list ->
controller.setData(list)
@@ -366,7 +366,7 @@ class RoomListFragment @Inject constructor(
)
}
}
- section.isExpanded.observe(viewLifecycleOwner) { _ ->
+ section.isExpanded.observe(viewLifecycleOwner) {
refreshCollapseStates()
}
controller.listener = this
@@ -400,9 +400,9 @@ class RoomListFragment @Inject constructor(
if (isAdded) {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.show()
- RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show()
- RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show()
- else -> Unit
+ RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show()
+ RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show()
+ RoomListDisplayMode.FILTERED -> Unit
}
}
}
@@ -422,28 +422,28 @@ class RoomListFragment @Inject constructor(
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
- is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
+ is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
}
- is RoomListQuickActionsSharedAction.NotificationsAll -> {
+ is RoomListQuickActionsSharedAction.NotificationsAll -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES))
}
is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY))
}
- is RoomListQuickActionsSharedAction.NotificationsMute -> {
+ is RoomListQuickActionsSharedAction.NotificationsMute -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE))
}
- is RoomListQuickActionsSharedAction.Settings -> {
+ is RoomListQuickActionsSharedAction.Settings -> {
navigator.openRoomProfile(requireActivity(), quickAction.roomId)
}
- is RoomListQuickActionsSharedAction.Favorite -> {
+ is RoomListQuickActionsSharedAction.Favorite -> {
roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE))
}
- is RoomListQuickActionsSharedAction.LowPriority -> {
+ is RoomListQuickActionsSharedAction.LowPriority -> {
roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY))
}
- is RoomListQuickActionsSharedAction.Leave -> {
+ is RoomListQuickActionsSharedAction.Leave -> {
promptLeaveRoom(quickAction.roomId)
}
}
@@ -484,21 +484,21 @@ class RoomListFragment @Inject constructor(
message = getString(R.string.room_list_catchup_empty_body)
)
}
- RoomListDisplayMode.PEOPLE ->
+ RoomListDisplayMode.PEOPLE ->
StateView.State.Empty(
title = getString(R.string.room_list_people_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_dm),
isBigImage = true,
message = getString(R.string.room_list_people_empty_body)
)
- RoomListDisplayMode.ROOMS ->
+ RoomListDisplayMode.ROOMS ->
StateView.State.Empty(
title = getString(R.string.room_list_rooms_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_room),
isBigImage = true,
message = getString(R.string.room_list_rooms_empty_body)
)
- else ->
+ RoomListDisplayMode.FILTERED ->
// Always display the content in this mode, because if the footer
StateView.State.Content
}
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 3aeff50516..8f7ab46191 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
@@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
class RoomListSectionBuilderGroup(
private val coroutineScope: CoroutineScope,
@@ -96,7 +97,6 @@ class RoomListSectionBuilderGroup(
true
) {
it.memberships = listOf(Membership.INVITE)
- it.roomCategoryFilter = RoomCategoryFilter.ALL
it.activeGroupId = actualGroupId
}
}
@@ -285,9 +285,6 @@ class RoomListSectionBuilderGroup(
}
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
- RoomSummaryQueryParams.Builder()
- .apply { builder.invoke(this) }
- .build()
- .let { block(it) }
+ block(roomSummaryQueryParams { builder.invoke(this) })
}
}
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 bd3e7bd1f9..0e3f61e186 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
@@ -43,14 +43,16 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.matrix.android.sdk.api.extensions.tryOrNull
-import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
+import org.matrix.android.sdk.api.query.SpaceFilter
+import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import timber.log.Timber
@@ -76,15 +78,15 @@ class RoomListSectionBuilderSpace(
val sections = mutableListOf()
val activeSpaceAwareQueries = mutableListOf()
when (mode) {
- RoomListDisplayMode.PEOPLE -> {
+ RoomListDisplayMode.PEOPLE -> {
// 4 sections Invites / Fav / Dms / Low Priority
buildDmSections(sections, activeSpaceAwareQueries)
}
- RoomListDisplayMode.ROOMS -> {
+ RoomListDisplayMode.ROOMS -> {
// 6 sections invites / Fav / Rooms / Low Priority / Server notice / Suggested rooms
buildRoomsSections(sections, activeSpaceAwareQueries)
}
- RoomListDisplayMode.FILTERED -> {
+ RoomListDisplayMode.FILTERED -> {
// Used when searching for rooms
buildFilteredSection(sections)
}
@@ -303,7 +305,6 @@ class RoomListSectionBuilderSpace(
countRoomAsNotif = true
) {
it.memberships = listOf(Membership.INVITE)
- it.roomCategoryFilter = RoomCategoryFilter.ALL
}
}
@@ -329,9 +330,9 @@ class RoomListSectionBuilderSpace(
{
it.memberships = Membership.activeMemberships()
},
- { qpm ->
+ { queryParams ->
val name = stringProvider.getString(R.string.bottom_action_rooms)
- val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(qpm)
+ val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams)
onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
@@ -378,29 +379,29 @@ class RoomListSectionBuilderSpace(
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
+ spaceFilter = roomId?.toActiveSpaceOrOrphanRooms()
)
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
}
})
}
- RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
+ RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) {
if (roomId != null) {
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
+ spaceFilter = SpaceFilter.ActiveSpace(roomId)
)
} else {
filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
- activeSpaceFilter = ActiveSpaceFilter.None
+ spaceFilter = null
)
}
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
}
})
}
- RoomListViewModel.SpaceFilterStrategy.NONE -> {
+ RoomListViewModel.SpaceFilterStrategy.NONE -> {
// we ignore current space for this one
}
}
@@ -437,31 +438,22 @@ class RoomListSectionBuilderSpace(
}
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
- RoomSummaryQueryParams.Builder()
- .apply { builder.invoke(this) }
- .build()
- .let { block(it) }
+ block(roomSummaryQueryParams { builder.invoke(this) })
}
internal fun RoomSummaryQueryParams.process(spaceFilter: RoomListViewModel.SpaceFilterStrategy, currentSpace: String?): RoomSummaryQueryParams {
return when (spaceFilter) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
+ spaceFilter = currentSpace?.toActiveSpaceOrOrphanRooms()
)
}
- RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
- if (currentSpace == null) {
- copy(
- activeSpaceFilter = ActiveSpaceFilter.None
- )
- } else {
- copy(
- activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace)
- )
- }
+ RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> {
+ copy(
+ spaceFilter = currentSpace?.let { SpaceFilter.ActiveSpace(it) }
+ )
}
- RoomListViewModel.SpaceFilterStrategy.NONE -> this
+ RoomListViewModel.SpaceFilterStrategy.NONE -> this
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt
index 70c5846646..5452b03992 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt
@@ -18,9 +18,9 @@ package im.vector.app.features.home.room.list
import android.view.HapticFeedbackConstants
import android.view.View
-import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
@@ -36,6 +36,7 @@ import im.vector.app.core.ui.views.PresenceStateImageView
import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@@ -45,48 +46,102 @@ import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_room)
abstract class RoomSummaryItem : VectorEpoxyModel() {
- @EpoxyAttribute lateinit var typingMessage: String
- @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute lateinit var matrixItem: MatrixItem
+ @EpoxyAttribute
+ lateinit var typingMessage: String
- @EpoxyAttribute lateinit var lastFormattedEvent: EpoxyCharSequence
- @EpoxyAttribute lateinit var lastEventTime: String
- @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
- @EpoxyAttribute var userPresence: UserPresence? = null
- @EpoxyAttribute var showPresence: Boolean = false
- @EpoxyAttribute var izPublic: Boolean = false
- @EpoxyAttribute var unreadNotificationCount: Int = 0
- @EpoxyAttribute var hasUnreadMessage: Boolean = false
- @EpoxyAttribute var hasDraft: Boolean = false
- @EpoxyAttribute var showHighlighted: Boolean = false
- @EpoxyAttribute var hasFailedSending: Boolean = false
- @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null
- @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null
- @EpoxyAttribute var showSelected: Boolean = false
+ @EpoxyAttribute
+ lateinit var avatarRenderer: AvatarRenderer
+
+ @EpoxyAttribute
+ lateinit var matrixItem: MatrixItem
+
+ @EpoxyAttribute
+ var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE
+
+ @EpoxyAttribute
+ lateinit var subtitle: String
+
+ @EpoxyAttribute
+ lateinit var lastFormattedEvent: EpoxyCharSequence
+
+ @EpoxyAttribute
+ lateinit var lastEventTime: String
+
+ @EpoxyAttribute
+ var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
+
+ @EpoxyAttribute
+ var userPresence: UserPresence? = null
+
+ @EpoxyAttribute
+ var showPresence: Boolean = false
+
+ @EpoxyAttribute @JvmField
+ var isPublic: Boolean = false
+
+ @EpoxyAttribute
+ var unreadNotificationCount: Int = 0
+
+ @EpoxyAttribute
+ var hasUnreadMessage: Boolean = false
+
+ @EpoxyAttribute
+ var hasDraft: Boolean = false
+
+ @EpoxyAttribute
+ var showHighlighted: Boolean = false
+
+ @EpoxyAttribute
+ var hasFailedSending: Boolean = false
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var itemLongClickListener: View.OnLongClickListener? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var itemClickListener: ClickListener? = null
+
+ @EpoxyAttribute
+ var showSelected: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
+
+ renderDisplayMode(holder)
holder.rootView.onClick(itemClickListener)
holder.rootView.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
itemLongClickListener?.onLongClick(it) ?: false
}
holder.titleView.text = matrixItem.getBestName()
- holder.lastEventTimeView.text = lastEventTime
- holder.lastEventView.text = lastFormattedEvent.charSequence
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
holder.draftView.isVisible = hasDraft
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.roomAvatarDecorationImageView.render(encryptionTrustLevel)
- holder.roomAvatarPublicDecorationImageView.isVisible = izPublic
+ holder.roomAvatarPublicDecorationImageView.isVisible = isPublic
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
renderSelection(holder, showSelected)
- holder.typingView.setTextOrHide(typingMessage)
- holder.lastEventView.isInvisible = holder.typingView.isVisible
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
}
+ private fun renderDisplayMode(holder: Holder) = when (displayMode) {
+ RoomListDisplayMode.ROOMS,
+ RoomListDisplayMode.PEOPLE,
+ RoomListDisplayMode.NOTIFICATIONS -> renderForDefaultDisplayMode(holder)
+ RoomListDisplayMode.FILTERED -> renderForFilteredDisplayMode(holder)
+ }
+
+ private fun renderForDefaultDisplayMode(holder: Holder) {
+ holder.subtitleView.text = lastFormattedEvent.charSequence
+ holder.lastEventTimeView.text = lastEventTime
+ holder.typingView.setTextOrHide(typingMessage)
+ holder.subtitleView.isInvisible = holder.typingView.isVisible
+ }
+
+ private fun renderForFilteredDisplayMode(holder: Holder) {
+ holder.subtitleView.text = subtitle
+ }
+
override fun unbind(holder: Holder) {
holder.rootView.setOnClickListener(null)
holder.rootView.setOnLongClickListener(null)
@@ -110,7 +165,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() {
val titleView by bind(R.id.roomNameView)
val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView)
val unreadIndentIndicator by bind(R.id.roomUnreadIndicator)
- val lastEventView by bind(R.id.roomLastEventView)
+ val subtitleView by bind(R.id.subtitleView)
val typingView by bind(R.id.roomTypingView)
val draftView by bind(R.id.roomDraftBadge)
val lastEventTimeView by bind(R.id.roomLastEventTimeView)
@@ -120,6 +175,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel() {
val roomAvatarPublicDecorationImageView by bind(R.id.roomAvatarPublicDecorationImageView)
val roomAvatarFailSendingImageView by bind(R.id.roomAvatarFailSendingImageView)
val roomAvatarPresenceImageView by bind(R.id.roomAvatarPresenceImageView)
- val rootView by bind(R.id.itemRoomLayout)
+ val rootView by bind(R.id.itemRoomLayout)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt
new file mode 100644
index 0000000000..8f2d949178
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.list
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.amulyakhare.textdrawable.TextDrawable
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.ui.views.PresenceStateImageView
+import im.vector.app.core.ui.views.ShieldImageView
+import im.vector.app.features.displayname.getBestName
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.RoomListDisplayMode
+import im.vector.app.features.themes.ThemeUtils
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
+import org.matrix.android.sdk.api.util.MatrixItem
+
+@EpoxyModelClass(layout = R.layout.item_room_centered)
+abstract class RoomSummaryItemCentered : VectorEpoxyModel() {
+
+ @EpoxyAttribute
+ lateinit var avatarRenderer: AvatarRenderer
+
+ @EpoxyAttribute
+ lateinit var matrixItem: MatrixItem
+
+ @EpoxyAttribute
+ var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE
+
+ @EpoxyAttribute
+ var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
+
+ @EpoxyAttribute
+ var userPresence: UserPresence? = null
+
+ @EpoxyAttribute
+ var showPresence: Boolean = false
+
+ @EpoxyAttribute @JvmField
+ var isPublic: Boolean = false
+
+ @EpoxyAttribute
+ var unreadNotificationCount: Int = 0
+
+ @EpoxyAttribute
+ var hasUnreadMessage: Boolean = false
+
+ @EpoxyAttribute
+ var hasDraft: Boolean = false
+
+ @EpoxyAttribute
+ var hasFailedSending: Boolean = false
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var itemLongClickListener: View.OnLongClickListener? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var itemClickListener: ClickListener? = null
+
+ @EpoxyAttribute
+ var showSelected: Boolean = false
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+
+ holder.rootView.onClick(itemClickListener)
+ holder.rootView.setOnLongClickListener {
+ it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ itemLongClickListener?.onLongClick(it) ?: false
+ }
+ holder.titleView.text = matrixItem.getBestName()
+ avatarRenderer.render(matrixItem, holder.avatarImageView)
+ holder.roomAvatarDecorationImageView.render(encryptionTrustLevel)
+ holder.roomAvatarPublicDecorationImageView.isVisible = isPublic
+ holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
+ renderSelection(holder, showSelected)
+ holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
+ }
+
+ override fun unbind(holder: Holder) {
+ holder.rootView.setOnClickListener(null)
+ holder.rootView.setOnLongClickListener(null)
+ avatarRenderer.clear(holder.avatarImageView)
+ super.unbind(holder)
+ }
+
+ private fun renderSelection(holder: Holder, isSelected: Boolean) {
+ if (isSelected) {
+ holder.avatarCheckedImageView.visibility = View.VISIBLE
+ val backgroundColor = ThemeUtils.getColor(holder.view.context, R.attr.colorPrimary)
+ val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
+ holder.avatarImageView.setImageDrawable(backgroundDrawable)
+ } else {
+ holder.avatarCheckedImageView.visibility = View.GONE
+ avatarRenderer.render(matrixItem, holder.avatarImageView)
+ }
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val titleView by bind(R.id.roomNameView)
+ val avatarCheckedImageView by bind(R.id.roomAvatarCheckedImageView)
+ val avatarImageView by bind(R.id.roomAvatarImageView)
+ val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView)
+ val roomAvatarPublicDecorationImageView by bind(R.id.roomAvatarPublicDecorationImageView)
+ val roomAvatarFailSendingImageView by bind(R.id.roomAvatarFailSendingImageView)
+ val roomAvatarPresenceImageView by bind(R.id.roomAvatarPresenceImageView)
+ val rootView by bind(R.id.itemRoomLayout)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index e831d446a5..9601ccf914 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
@@ -49,6 +50,7 @@ class RoomSummaryItemFactory @Inject constructor(
roomSummary: RoomSummary,
roomChangeMembershipStates: Map,
selectedRoomIds: Set,
+ displayMode: RoomListDisplayMode,
listener: RoomListListener?
): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
@@ -56,7 +58,9 @@ class RoomSummaryItemFactory @Inject constructor(
val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
createInvitationItem(roomSummary, changeMembershipState, listener)
}
- else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
+ else -> createRoomItem(
+ roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
+ )
}
}
@@ -113,9 +117,11 @@ class RoomSummaryItemFactory @Inject constructor(
fun createRoomItem(
roomSummary: RoomSummary,
selectedRoomIds: Set,
+ displayMode: RoomListDisplayMode,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
): VectorEpoxyModel<*> {
+ val subtitle = getSearchResultSubtitle(roomSummary)
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
@@ -126,28 +132,84 @@ class RoomSummaryItemFactory @Inject constructor(
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
}
+
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
- return RoomSummaryItem_()
- .id(roomSummary.roomId)
- .avatarRenderer(avatarRenderer)
- // We do not display shield in the room list anymore
- // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
- .izPublic(roomSummary.isPublic)
- .showPresence(roomSummary.isDirect)
- .userPresence(roomSummary.directUserPresence)
- .matrixItem(roomSummary.toMatrixItem())
- .lastEventTime(latestEventTime)
- .typingMessage(typingMessage)
- .lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence())
- .showHighlighted(showHighlighted)
- .showSelected(showSelected)
- .hasFailedSending(roomSummary.hasFailedSending)
- .unreadNotificationCount(unreadCount)
- .hasUnreadMessage(roomSummary.hasUnreadMessages)
- .hasDraft(roomSummary.userDrafts.isNotEmpty())
- .itemLongClickListener { _ ->
- onLongClick?.invoke(roomSummary) ?: false
- }
- .itemClickListener { onClick?.invoke(roomSummary) }
+
+ return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
+ createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
+ } else {
+ createRoomSummaryItem(
+ roomSummary, displayMode, subtitle, latestEventTime, typingMessage,
+ latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick
+ )
+ }
+ }
+
+ private fun createRoomSummaryItem(
+ roomSummary: RoomSummary,
+ displayMode: RoomListDisplayMode,
+ subtitle: String,
+ latestEventTime: String,
+ typingMessage: String,
+ latestFormattedEvent: CharSequence,
+ showHighlighted: Boolean,
+ showSelected: Boolean,
+ unreadCount: Int,
+ onClick: ((RoomSummary) -> Unit)?,
+ onLongClick: ((RoomSummary) -> Boolean)?
+ ) = RoomSummaryItem_()
+ .id(roomSummary.roomId)
+ .avatarRenderer(avatarRenderer)
+ // We do not display shield in the room list anymore
+ // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
+ .displayMode(displayMode)
+ .subtitle(subtitle)
+ .isPublic(roomSummary.isPublic)
+ .showPresence(roomSummary.isDirect)
+ .userPresence(roomSummary.directUserPresence)
+ .matrixItem(roomSummary.toMatrixItem())
+ .lastEventTime(latestEventTime)
+ .typingMessage(typingMessage)
+ .lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence())
+ .showHighlighted(showHighlighted)
+ .showSelected(showSelected)
+ .hasFailedSending(roomSummary.hasFailedSending)
+ .unreadNotificationCount(unreadCount)
+ .hasUnreadMessage(roomSummary.hasUnreadMessages)
+ .hasDraft(roomSummary.userDrafts.isNotEmpty())
+ .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
+ .itemClickListener { onClick?.invoke(roomSummary) }
+
+ private fun createCenteredRoomSummaryItem(
+ roomSummary: RoomSummary,
+ displayMode: RoomListDisplayMode,
+ showSelected: Boolean,
+ unreadCount: Int,
+ onClick: ((RoomSummary) -> Unit)?,
+ onLongClick: ((RoomSummary) -> Boolean)?
+ ) = RoomSummaryItemCentered_()
+ .id(roomSummary.roomId)
+ .avatarRenderer(avatarRenderer)
+ // We do not display shield in the room list anymore
+ // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
+ .displayMode(displayMode)
+ .isPublic(roomSummary.isPublic)
+ .showPresence(roomSummary.isDirect)
+ .userPresence(roomSummary.directUserPresence)
+ .matrixItem(roomSummary.toMatrixItem())
+ .showSelected(showSelected)
+ .hasFailedSending(roomSummary.hasFailedSending)
+ .unreadNotificationCount(unreadCount)
+ .hasUnreadMessage(roomSummary.hasUnreadMessages)
+ .hasDraft(roomSummary.userDrafts.isNotEmpty())
+ .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
+ .itemClickListener { onClick?.invoke(roomSummary) }
+
+ private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
+ val userId = roomSummary.directUserId
+ val spaceName = roomSummary.spaceParents?.firstOrNull()?.roomSummary?.name
+ val canonicalAlias = roomSummary.canonicalAlias
+
+ return (userId ?: spaceName ?: canonicalAlias).orEmpty()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt
index 75aaee45cb..2eb8921fd5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt
@@ -16,17 +16,19 @@
package im.vector.app.features.home.room.list
+import im.vector.app.features.home.RoomListDisplayMode
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryListController(
- private val roomSummaryItemFactory: RoomSummaryItemFactory
+ private val roomSummaryItemFactory: RoomSummaryItemFactory,
+ private val displayMode: RoomListDisplayMode
) : CollapsableTypedEpoxyController>() {
var listener: RoomListListener? = null
override fun buildModels(data: List?) {
data?.forEach {
- add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), listener))
+ add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener))
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt
index e9cbc69215..445438eec9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt
@@ -19,11 +19,13 @@ package im.vector.app.features.home.room.list
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.utils.createUIHandler
+import im.vector.app.features.home.RoomListDisplayMode
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryPagedController(
- private val roomSummaryItemFactory: RoomSummaryItemFactory
+ private val roomSummaryItemFactory: RoomSummaryItemFactory,
+ private val displayMode: RoomListDisplayMode
) : PagedListEpoxyController(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
@@ -57,6 +59,6 @@ class RoomSummaryPagedController(
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
// for place holder if enabled
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
- return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), listener)
+ return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt
index c86d8ab243..f72698048d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt
@@ -16,18 +16,19 @@
package im.vector.app.features.home.room.list
+import im.vector.app.features.home.RoomListDisplayMode
import javax.inject.Inject
class RoomSummaryPagedControllerFactory @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory
) {
- fun createRoomSummaryPagedController(): RoomSummaryPagedController {
- return RoomSummaryPagedController(roomSummaryItemFactory)
+ fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController {
+ return RoomSummaryPagedController(roomSummaryItemFactory, displayMode)
}
- fun createRoomSummaryListController(): RoomSummaryListController {
- return RoomSummaryListController(roomSummaryItemFactory)
+ fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController {
+ return RoomSummaryListController(roomSummaryItemFactory, displayMode)
}
fun createSuggestedRoomListController(): SuggestedRoomListController {
diff --git a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt
index 8f424af9ec..ec8638fdbb 100644
--- a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt
@@ -17,20 +17,10 @@
package im.vector.app.features.location
import android.app.Activity
-import im.vector.app.core.utils.openAppSettingsPage
class DefaultLocationSharingNavigator constructor(val activity: Activity?) : LocationSharingNavigator {
- override var goingToAppSettings: Boolean = false
-
override fun quit() {
activity?.finish()
}
-
- override fun goToAppSettings() {
- activity?.let {
- goingToAppSettings = true
- openAppSettingsPage(it)
- }
- }
}
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 251e7003eb..1b25f3fcec 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
@@ -32,9 +32,9 @@ import com.mapbox.mapboxsdk.maps.MapView
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
import im.vector.app.core.utils.checkPermissions
+import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentLocationSharingBinding
import im.vector.app.features.home.AvatarRenderer
@@ -100,11 +100,6 @@ class LocationSharingFragment @Inject constructor(
override fun onResume() {
super.onResume()
views.mapView.onResume()
- if (locationSharingNavigator.goingToAppSettings) {
- locationSharingNavigator.goingToAppSettings = false
- // retry to start live location
- tryStartLiveLocationSharing()
- }
}
override fun onPause() {
@@ -162,18 +157,6 @@ class LocationSharingFragment @Inject constructor(
.show()
}
- private fun handleMissingBackgroundLocationPermission() {
- MaterialAlertDialogBuilder(requireActivity())
- .setTitle(R.string.location_in_background_missing_permission_dialog_title)
- .setMessage(R.string.location_in_background_missing_permission_dialog_content)
- .setPositiveButton(R.string.settings) { _, _ ->
- locationSharingNavigator.goToAppSettings()
- }
- .setNegativeButton(R.string.action_not_now, null)
- .setCancelable(false)
- .show()
- }
-
private fun initLocateButton() {
views.mapView.locateButton.setOnClickListener {
viewModel.handle(LocationSharingAction.ZoomToUserLocation)
@@ -181,7 +164,7 @@ class LocationSharingFragment @Inject constructor(
}
private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) {
- views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
+ views.mapView.zoomToLocation(event.userLocation)
}
private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) {
@@ -212,30 +195,16 @@ class LocationSharingFragment @Inject constructor(
}
private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
- if (allGranted && checkPermissions(PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, requireActivity(), backgroundLocationResultLauncher)) {
- startLiveLocationSharing()
- } else if (deniedPermanently) {
- handleMissingBackgroundLocationPermission()
- }
- }
-
- private val backgroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) {
startLiveLocationSharing()
} else if (deniedPermanently) {
- handleMissingBackgroundLocationPermission()
+ activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
}
}
private fun tryStartLiveLocationSharing() {
// we need to re-check foreground location to be sure it has not changed after landing on this screen
- if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher) &&
- checkPermissions(
- PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING,
- requireActivity(),
- backgroundLocationResultLauncher,
- R.string.location_in_background_missing_permission_dialog_content
- )) {
+ if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) {
startLiveLocationSharing()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt
index 8927da9239..6f4759a1e4 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt
@@ -17,7 +17,5 @@
package im.vector.app.features.location
interface LocationSharingNavigator {
- var goingToAppSettings: Boolean
fun quit()
- fun goToAppSettings()
}
diff --git a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt
new file mode 100644
index 0000000000..cbfdf1dfda
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt
@@ -0,0 +1,44 @@
+/*
+ * 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
+
+import com.mapbox.mapboxsdk.camera.CameraPosition
+import com.mapbox.mapboxsdk.constants.MapboxConstants
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.geometry.LatLngBounds
+import com.mapbox.mapboxsdk.maps.MapboxMap
+
+fun MapboxMap?.zoomToLocation(locationData: LocationData, preserveCurrentZoomLevel: Boolean = false) {
+ val zoomLevel = if (preserveCurrentZoomLevel && this?.cameraPosition != null) {
+ cameraPosition.zoom
+ } else {
+ INITIAL_MAP_ZOOM_IN_PREVIEW
+ }
+ this?.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(locationData.latitude, locationData.longitude))
+ .zoom(zoomLevel)
+ .build()
+}
+
+fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) {
+ this?.getCameraForLatLngBounds(latLngBounds)?.let { camPosition ->
+ // unZoom a little to avoid having pins exactly at the edges of the map
+ cameraPosition = CameraPosition.Builder(camPosition)
+ .zoom((camPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble()))
+ .build()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
index 69e4e9fc20..dd2a56fb3a 100644
--- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
@@ -25,7 +25,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.marginBottom
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
-import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
@@ -164,7 +163,7 @@ class MapTilerMapView @JvmOverloads constructor(
state.userLocationData?.let { locationData ->
if (!initZoomDone || !state.zoomOnlyOnce) {
- zoomToLocation(locationData.latitude, locationData.longitude)
+ zoomToLocation(locationData)
initZoomDone = true
}
@@ -180,12 +179,9 @@ class MapTilerMapView @JvmOverloads constructor(
}
}
- fun zoomToLocation(latitude: Double, longitude: Double) {
+ fun zoomToLocation(locationData: LocationData) {
Timber.d("## Location: zoomToLocation")
- mapRefs?.map?.cameraPosition = CameraPosition.Builder()
- .target(LatLng(latitude, longitude))
- .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
- .build()
+ mapRefs?.map?.zoomToLocation(locationData)
}
fun getLocationOfMapCenter(): LocationData? =
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt
new file mode 100644
index 0000000000..91f6999e2c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.live.map
+
+import androidx.lifecycle.asFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.mapLatest
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.getRoom
+import javax.inject.Inject
+
+class GetListOfUserLiveLocationUseCase @Inject constructor(
+ private val session: Session,
+ private val userLiveLocationViewStateMapper: UserLiveLocationViewStateMapper,
+) {
+
+ fun execute(roomId: String): Flow> {
+ return session.getRoom(roomId)
+ ?.locationSharingService()
+ ?.getRunningLiveLocationShareSummaries()
+ ?.asFlow()
+ ?.mapLatest { it.mapNotNull { summary -> userLiveLocationViewStateMapper.map(summary) } }
+ ?: emptyFlow()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt
new file mode 100644
index 0000000000..c07104e72c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.live.map
+
+import android.content.Context
+import com.airbnb.epoxy.EpoxyController
+import im.vector.app.R
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.DateProvider
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.resources.toTimestamp
+import im.vector.app.core.time.Clock
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.location.live.map.bottomsheet.LiveLocationUserItem
+import im.vector.app.features.location.live.map.bottomsheet.liveLocationUserItem
+import javax.inject.Inject
+
+class LiveLocationBottomSheetController @Inject constructor(
+ private val avatarRenderer: AvatarRenderer,
+ private val vectorDateFormatter: VectorDateFormatter,
+ private val stringProvider: StringProvider,
+ private val clock: Clock,
+ private val context: Context,
+) : EpoxyController() {
+
+ interface Callback {
+ fun onUserSelected(userId: String)
+ fun onStopLocationClicked()
+ }
+
+ private var userLocations: List? = null
+ var callback: Callback? = null
+
+ fun setData(userLocations: List) {
+ this.userLocations = userLocations
+ requestModelBuild()
+ }
+
+ override fun buildModels() {
+ val currentUserLocations = userLocations ?: return
+ val host = this
+
+ val userItemCallback = object : LiveLocationUserItem.Callback {
+ override fun onUserSelected(userId: String) {
+ host.callback?.onUserSelected(userId)
+ }
+
+ override fun onStopSharingClicked() {
+ host.callback?.onStopLocationClicked()
+ }
+ }
+
+ currentUserLocations.forEach { liveLocationViewState ->
+ val remainingTime = getFormattedLocalTimeEndOfLive(liveLocationViewState.endOfLiveTimestampMillis)
+ liveLocationUserItem {
+ id(liveLocationViewState.matrixItem.id)
+ callback(userItemCallback)
+ matrixItem(liveLocationViewState.matrixItem)
+ stringProvider(host.stringProvider)
+ clock(host.clock)
+ avatarRenderer(host.avatarRenderer)
+ remainingTime(remainingTime)
+ locationUpdateTimeMillis(liveLocationViewState.locationTimestampMillis)
+ showStopSharingButton(liveLocationViewState.showStopSharingButton)
+ }
+ }
+ }
+
+ private fun getFormattedLocalTimeEndOfLive(endOfLiveDateTimestampMillis: Long?): String {
+ val endOfLiveDateTime = DateProvider.toLocalDateTime(endOfLiveDateTimestampMillis)
+ val formattedDateTime = endOfLiveDateTime.toTimestamp().let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) }
+ return stringProvider.getString(R.string.location_share_live_until, formattedDateTime)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
new file mode 100644
index 0000000000..336f688434
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.live.map.bottomsheet
+
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.time.Clock
+import im.vector.app.core.utils.TextUtils
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.lib.core.utils.timer.CountUpTimer
+import org.matrix.android.sdk.api.util.MatrixItem
+import org.threeten.bp.Duration
+
+@EpoxyModelClass(layout = R.layout.item_live_location_users_bottom_sheet)
+abstract class LiveLocationUserItem : VectorEpoxyModel() {
+
+ interface Callback {
+ fun onUserSelected(userId: String)
+ fun onStopSharingClicked()
+ }
+
+ @EpoxyAttribute
+ var callback: Callback? = null
+
+ @EpoxyAttribute
+ lateinit var matrixItem: MatrixItem
+
+ @EpoxyAttribute
+ lateinit var avatarRenderer: AvatarRenderer
+
+ @EpoxyAttribute
+ lateinit var stringProvider: StringProvider
+
+ @EpoxyAttribute
+ lateinit var clock: Clock
+
+ @EpoxyAttribute
+ var remainingTime: String? = null
+
+ @EpoxyAttribute
+ var locationUpdateTimeMillis: Long? = null
+
+ @EpoxyAttribute
+ var showStopSharingButton: Boolean = false
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ avatarRenderer.render(matrixItem, holder.itemUserAvatarImageView)
+ holder.itemUserDisplayNameTextView.text = matrixItem.displayName
+ holder.itemRemainingTimeTextView.text = remainingTime
+
+ holder.itemStopSharingButton.isVisible = showStopSharingButton
+ if (showStopSharingButton) {
+ holder.itemStopSharingButton.onClick {
+ callback?.onStopSharingClicked()
+ }
+ }
+
+ stopTimer(holder)
+ holder.timer = CountUpTimer(1000).apply {
+ tickListener = object : CountUpTimer.TickListener {
+ override fun onTick(milliseconds: Long) {
+ holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
+ }
+ }
+ resume()
+ }
+
+ holder.view.setOnClickListener { callback?.onUserSelected(matrixItem.id) }
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ stopTimer(holder)
+ }
+
+ private fun stopTimer(holder: Holder) {
+ holder.timer?.stop()
+ holder.timer = null
+ }
+
+ private fun getFormattedLastUpdatedAt(locationUpdateTimeMillis: Long?): String {
+ if (locationUpdateTimeMillis == null) return ""
+ val elapsedTime = clock.epochMillis() - locationUpdateTimeMillis
+ val duration = Duration.ofMillis(elapsedTime.coerceAtLeast(0L))
+ val formattedDuration = TextUtils.formatDurationWithUnits(stringProvider, duration, appendSeconds = false)
+ return stringProvider.getString(R.string.live_location_bottom_sheet_last_updated_at, formattedDuration)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ var timer: CountUpTimer? = null
+ val itemUserAvatarImageView by bind(R.id.itemUserAvatarImageView)
+ val itemUserDisplayNameTextView by bind(R.id.itemUserDisplayNameTextView)
+ val itemRemainingTimeTextView by bind(R.id.itemRemainingTimeTextView)
+ val itemLastUpdatedAtTextView by bind(R.id.itemLastUpdatedAtTextView)
+ val itemStopSharingButton by bindMengubah avatar ruangan saat iniMengubah avatar Anda di ruangan saat ini sajaMenampilkan
- Tidak Tersedia
+ Tidak tersediaLuringDaringPilih homeserver
@@ -2445,4 +2441,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
\n
\nDicatat bahwa tindakan ini akan memulai ulang aplikasinya dan mungkin membutuhkan beberapa waktu.Permintaan sinkronisasi awal
+ Tampilkan info profil (avatar dan nama tampilan) terkini untuk semua pesan.
+ Tampilkan info pengguna terkini
+ Sibuk
+ Cadangan memiliki tandatangan yang valid dari pengguna ini.
diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml
index 892d56b795..19000920e0 100644
--- a/vector/src/main/res/values-is/strings.xml
+++ b/vector/src/main/res/values-is/strings.xml
@@ -1060,7 +1060,7 @@
TegundSenda sérsniðinn atburðSkoða stöðu spjallrásar
- Ekki tiltækt
+ FjarverandiÓnettengtNettengtEkki tilkynna
@@ -1724,7 +1724,6 @@
Athugun á Play-þjónustumUppfæra einkaspjallrásUppfæra almenningsspjallrás
- Yfirgefa allar spjallrásir og svæðiBjóða með tölvupóstiEinkasvæði til að skipuleggja spjallrásirnar þínarHverjum ertu að vinna með\?
@@ -1932,9 +1931,6 @@
Tilkynna afkóðunarvillur sjálfvirkt.Bættu svæði við eitthvað svæði sem þú stýrir.Bæta við fyrirliggjandi spjallrásum og svæði
- Veldu það sem á að yfirgefa
- Yfirgefa tilteknar spjallrásir og svæði…
- Ekki yfirgefa neinar spjallrásir eða svæðiErtu viss um að þú viljir yfirgefa %s\?Einkasvæði fyrir þig og félaga í teyminu þínuBæta við í uppgefið svæði
@@ -1976,4 +1972,54 @@
Spjallþræðir í bráðlegri Beta-útgáfu 🎉Ráðfæri við %1$sRáðfæra fyrst
+ Deiling á skjá er í vinnslu
+ ${app_name} skjádeiling
+ Deiling staðsetningar er í vinnslu
+ ${app_name} rauntímastaðsetning
+ Hleð inn rauntímastaðsetningu…
+ 8 klukkustundir
+ 1 klukkustund
+ 15 mínútur
+ Deildu rauntímastaðsetningu þinni í
+ (%1$s)
+ %1$s (%2$s)
+ Gat ekki spilað %1$s
+ Setja %1$s í bið
+ Spila %1$s
+ %1$d mínútur %2$d sekúndur
+ %1$s, %2$s, %3$s
+ Upptekinn
+ Flytja yfir í %1$s
+ Útbý SSSS-lykil úr endurheimtulykli
+ Útbý SSSS-lykil úr lykilsetningu (%s)
+ Útbý SSSS-lykil úr lykilsetningu
+ Athuga öryggisafritunarlykil (%s)
+ Þú ert búin/n!
+ Deildu raunstaðsetningu sinni
+ Upphafleg samstilling…
+ Séð af
+ Sleppa þessu skrefi
+ Vista og halda áfram
+ Kjörstillingarnar þínar hafa verið vistaðar.
+ Nú ertu tilbúin(n)!
+ Hefjumst handa
+ Þú getur breytt þessu hvenær sem er.
+ Bættu við auðkennismynd
+ Þú getur breytt þessu síðar
+ Birtingarnafn
+ Veldu birtingarnafn
+ Til hamingju!
+ Virkja ítarlega atvikaskráningu.
+ BETA-prófunarútgáfa
+ Gefðu umsögn
+ BETA-prófunarútgáfa
+ Næ í útgáfu öryggisafrits…
+ Ónettengdur hamur
+ Viðvera
+ Hætta skjádeilingu
+ Deila skjá
+ Kanna nánar
+ Prófaðu það
+ Gera óvirkt
+ Upphafleg samstillingarbeiðni
diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml
index 3eec2c5f14..84b2285cc4 100644
--- a/vector/src/main/res/values-it/strings.xml
+++ b/vector/src/main/res/values-it/strings.xml
@@ -2192,10 +2192,6 @@
Aggiungi uno spazio ad un altro che gestisci.Aggiungi spazi esistentiAggiungi stanze esistenti
- Scegli da cosa uscire
- Esci da stanze e spazi specifici…
- Non uscire da alcuna stanza e spazio
- Esci da tutte le stanze e gli spaziVuoi davvero uscire da %s\?Termina configurazioneInvita per email, trova contatti e altro…
@@ -2242,7 +2238,7 @@
Imposta il nome della stanzaSmetti di ignorare un utente, mostrando i suoi messaggi successiviIgnora un utente, non mostrandoti i suoi messaggi
- Non disponibile
+ AssenteOfflineOnlineScegli homeserver
@@ -2324,7 +2320,7 @@
Apri fotocameraIl tuo sistema invierà automaticamente i log quando si verifica un errore di decifrazioneAuto-segnala errori di decifrazione.
- Sovrascrivi colore nick
+ Sovrascrivi colore nome visualizzatoHo già un accountMessaggistica sicura.Tu hai il controllo.
@@ -2481,4 +2477,8 @@
\n
\nNota che questa azione riavvierà l\'app e potrebbe richiedere del tempo.Richiesta di sincronizzazione iniziale
+ Mostra le informazioni di profilo più recenti (avatar e nome visualizzato) per tutti i messaggi.
+ Mostra info utente più recenti
+ Occupato
+ Il backup ha una firma valida da questo utente.
diff --git a/vector/src/main/res/values-iw/strings.xml b/vector/src/main/res/values-iw/strings.xml
index 2a40246e3b..65071af708 100644
--- a/vector/src/main/res/values-iw/strings.xml
+++ b/vector/src/main/res/values-iw/strings.xml
@@ -2272,10 +2272,6 @@
הוסף מרחבים קיימיםהוסף חדרים קיימיםהוסף חדרים ומרחבים קיימים
- בחר דברים להשאיר
- השאירו חדרים ומרחבים ספציפיים…
- אל תעזובו חדרים וא מרחבים
- עזבו את כל החדרים והמרחביםאתה המנהל היחיד של המרחב הזה. עזיבה תגרום לכך שאף אחד לא ישלוט על המרחב.לא תוכל להצטרף מחדש אלא אם כן תוזמן מחדש.אתה האדם היחיד כאן. אם תעזוב, אף אחד לא יוכל להצטרף בעתיד, כולל אותך.
diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml
index 8852358966..3510db2dac 100644
--- a/vector/src/main/res/values-ja/strings.xml
+++ b/vector/src/main/res/values-ja/strings.xml
@@ -1653,7 +1653,6 @@
matrix.orgを選択既存のスペースを追加既存のルームを追加
- 全てのルームとスペースから退出セットアップを終了ランダムスペースを作成しています…
@@ -1879,8 +1878,6 @@
%sに招待ユーザー名かメールアドレスで招待
- どのルームやスペースからも退出しない
- 退出するルームとスペースを選択%sを退出してよろしいですか?スペースは、ルームや連絡先をグループ化する新しい方法です。招待されています
@@ -1939,7 +1936,6 @@
Element Matrix Servicesのアドレス%1$sにサインイン誰と最もよく会話しますか?
- 特定のルームとスペースから退出…非公開で招待が必要なルームは表示されていません。
\nルームを追加する権限はありません。非公開で招待が必要なルームは表示されていません。
diff --git a/vector/src/main/res/values-lo/strings.xml b/vector/src/main/res/values-lo/strings.xml
index 3d12927ac7..af65313257 100644
--- a/vector/src/main/res/values-lo/strings.xml
+++ b/vector/src/main/res/values-lo/strings.xml
@@ -1471,10 +1471,6 @@
ເພີ່ມພື້ນທີ່ທີ່ມີຢູ່ເພີ່ມຫ້ອງທີ່ມີຢູ່ເພີ່ມຫ້ອງ ແລະພື້ນທີ່ທີ່ມີຢູ່
- ເລືອກສິ່ງທີ່ຕ້ອງອອກໄປ
- ອອກຈາກຫ້ອງ ແລະພື້ນທີ່ສະເພາະ…
- ຢ່າອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ໃດໆ
- ອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ທັງໝົດທ່ານພຽງຜູ້ທີ່ເບິ່ງແຍງລະບົບພື້ນທີ່ນີ້. ການອອກຈາກລະບົບນັ້ນຫມາຍຄວາມວ່າບໍ່ມີໃຜຄວບຄຸມມັນ.ທ່ານຈະບໍ່ສາມາດເຂົ້າຮ່ວມຄືນໃໝ່ໄດ້ເວັ້ນເສຍແຕ່ວ່າທ່ານໄດ້ຮັບເຊີນຄືນໃໝ່.ທ່ານເປັນຄົນດຽວຢູ່ທີ່ນີ້. ຖ້າທ່ານອອກໄປ, ບໍ່ມີໃຜຈະສາມາດເຂົ້າຮ່ວມໃນອະນາຄົດ, ລວມທັງທ່ານ.
@@ -2018,7 +2014,7 @@
ສົ່ງເຫດການທີ່ກຳນົດເອງສຳຫຼວດສະຖານະຫ້ອງເຄື່ອງມືພັດທະນາ
- ບໍ່ສາມາດໃຊ້ໄດ້
+ ບໍ່ຢູ່ອອຟລາຍອອນລາຍຫ້ອງສາທາລະນະ
@@ -2243,7 +2239,7 @@
ກຳນົດເອງຜູ້ຄວບຄຸມຜູ້ເບິ່ງແຍງລະບົບ
- ລົບລ້າງສີ nick
+ ໂຊທັບສີຂອງຊື່ນີ້ກຳລັງອອກຈາກຫ້ອງ…ອອກໄປອອກຈາກຫ້ອງ
@@ -2441,4 +2437,8 @@
ຢຸດການບັນທຶກຢຸດຂໍ້ຄວາມສຽງຊົ່ວຄາວຫຼິ້ນຂໍ້ຄວາມສຽງ
+ ໂຊຂໍ້ມູນໂປຟາຍລ້າສຸດ (ໂຕກະຕູນ ແລະ ຊື່ສະແດງ) ສຳລັບທຸກໆຂໍ້ຄວາມ.
+ ໂຊຂໍ້ມູນຜູ້ໃຊ້ລ້າສຸດ
+ ບໍ່ວ່າງ
+ ແບັກອັບມີລາຍເຊັນຖືກຕ້ອງຈາກຜູ້ໃຊ້ນີ້.
diff --git a/vector/src/main/res/values-lo/strings_no_weblate.xml b/vector/src/main/res/values-lo/strings_no_weblate.xml
new file mode 100644
index 0000000000..d4c584c65f
--- /dev/null
+++ b/vector/src/main/res/values-lo/strings_no_weblate.xml
@@ -0,0 +1,8 @@
+
+
+
+ lo
+ LA
+ Laoo
+
+
diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml
index 3397415ab7..a8fd805668 100644
--- a/vector/src/main/res/values-nl/strings.xml
+++ b/vector/src/main/res/values-nl/strings.xml
@@ -1691,7 +1691,7 @@
Aangepaste gebeurtenis versturenKamer status ontdekkenOntwikkel tools
- Niet beschikbaar
+ AfwezigOfflineOnlineOpenbare kamer
@@ -2268,10 +2268,6 @@
Bestaande spaces toevoegenBestaande kamers toevoegenBestaande kamers en space toevoegen
- Kies dingen om te verlaten
- Specifieke kamers en spaces verlaten…
- Verlaat geen kamers en spaces
- Verlaat alle kamers en spacesU kunt pas weer deelnemen als u opnieuw wordt uitgenodigd.U bent de enige persoon hier. Als u weggaat, kan niemand meer meedoen, ook u niet.Weet u zeker dat u %s wilt verlaten\?
@@ -2334,7 +2330,7 @@
Word lid van de Space met het opgegeven idToevoegen aan een SpaceMaak een Space
- Bijnaam kleur overschrijven
+ Kleur weergavenaam overschrijvenIk heb al een accountVeilig berichtenverkeer.U heeft de controle.
@@ -2490,4 +2486,8 @@
\n
\nHoud er rekening mee dat deze actie de app opnieuw zal starten en dat dit enige tijd kan duren.Initieel synchronisatieverzoek
+ Toon de laatste profielinformatie (avatar en weergavenaam) voor alle berichten.
+ Toon laatste persoonsinformatie
+ Bezet
+ Back-up heeft een geldige handtekening van deze persoon.
diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml
index d6fef1c620..7738b099df 100644
--- a/vector/src/main/res/values-pl/strings.xml
+++ b/vector/src/main/res/values-pl/strings.xml
@@ -736,8 +736,8 @@
Zaproszenia, usunięcia i bany pozostają nienaruszone.Wysyłaj wiadomości za pomocą klawisza enterPrzycisk enter na klawiaturze programowej wyśle wiadomość zamiast wprowadzania łamanania linii
- Znajdź
- Zarządzaj ustawieniami wyszukiwania.
+ Ustawienia wyszukiwania
+ Ustal jak inni mogą odnaleść twoje konto.MediaDomyślne źródło mediówOdzyskiwanie zaszyforwanych wiadomości
@@ -1619,7 +1619,7 @@
Jeżeli chcesz zresetować kod PIN, naciśnij Zapomnij kod PIN aby wylogować i zresetować.Włącz kod PINSkonfiguruj ochronę
- Ochrona dostępu poprzez kod PIN oraz biometrię.
+ Blokada aplikacji poprzez kod PIN oraz biometrię.Ochrona dostępuAby zresetować kod PIN musisz się ponownie zalogować i utworzyć nowy.Nowy kod PIN
@@ -1851,7 +1851,7 @@
%d nie odebranych połączeń głosowych%d nie odebranych połączeń głosowych
- Połączenie przychodzące…
+ Dzwoni…Błąd autoryzacji, niepoprawne dane logowaniaWybierz serwer domowyNie można było połączyć się z serwerem domowym %s. Proszę sprawdzić link lub wybrać serwer domowy manualnie.
@@ -1964,15 +1964,11 @@
Szukasz kogoś , kto nie jest w %s\?%s Cię zapraszaZostałeś zaproszony
- Przestrzenie są nową metodą na zarządzanie pokojami i osobami.
+ Przestrzenie są nową metodą na grupowanie razem wielu pokoi i osób.Dodaj przestrzeń do jakiejkolwiek przestrzeni którą zarządzasz.Dodaj istniejące przestrzenieDodaj istniejące pokojeDodaj istniejące pokoje i przestrzenie
- Wybierz aby opuścić
- Opuść wybrane pokoje i przestrzenie…
- Nie opuszczaj żadnych pokojów i przestrzeni
- Opuść wszystkie pokoje i przestrzenieJesteś jedynym administratorem tej przestrzeni. Opuszczenie jej oznacza brak kontroli nad nią.Nie będziesz w stanie ponownie dołączyć, do momentu kiedy nie zostaniesz ponownie zaproszony.Jesteś jedyną osoba tutaj. Jeśli wyjdziesz, nikt nie będzie w stanie dołączyć w przyszłości, włączając Ciebie.
@@ -2083,7 +2079,7 @@
Podziel się opiniąNie udało się przesłać opinii (%s)Dziękujemy, Twoja opinia została wysłana
- Zachęcamy do kontaktu, jeśli masz dodatkowe pytania
+ Pozwalam na kontakt ze mną w razie dodatkowych pytańUżywasz przestrzeni w wersji beta. Ta opinia pomoże nam w tworzeniu kolejnych wersji. Twoja platforma i nazwa użytkownika zostaną odnotowane, abyśmy mogli w pełni wykorzystać Twoje sugestie.Prześlij opinię o przestrzeniachStwórz nową przestrzeń
@@ -2258,7 +2254,7 @@
Edytuj treśćPoznaj stan pokojuNarzędzia deweloperskie
- Niedostępny
+ NieobecnyPokój publicznyNie powiadamiajPowiadom bez dźwięku
@@ -2321,7 +2317,7 @@
Usunąłeś(aś) %1$s jako adresy tego pokoju.Usunąłeś(aś) %1$s jako adresy tego pokoju.
- Nadpisz kolor nicku
+ Zastąp kolor wyświetlanej nazwyPosiadam już kontoPołącz się z każdym.Ty jesteś w kontroli.
@@ -2583,4 +2579,8 @@
Udostępnij ekran- Niektórzy użytkownicy przestali być ignorowaniPoczątkowe żądanie synchronizacji
+ Kopia zapasowa ma poprawny podpis tego użytkownika.
+ Pokazuj aktualny stan profilu (obraz i nazwa) w całej historii konwersacji.
+ Pokazuj najnowsze informacje o użytkowniku
+ Zajęty
diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml
index 97896dec64..4bc0888b01 100644
--- a/vector/src/main/res/values-pt-rBR/strings.xml
+++ b/vector/src/main/res/values-pt-rBR/strings.xml
@@ -2198,10 +2198,6 @@
Adicione um espaço a qualquer espaço que você gerencia.Adicionar espaços existentesAdicionar salas existentes
- Escolha coisas para sair
- Sair de salas e espaços específicos…
- Não sair de nenhuma sala ou espaço
- Sair de todas as salas e espaçosVocê tem certeza que você quer sair de %s\?Descoberta (%s)Terminar configuração
@@ -2251,7 +2247,7 @@
Define o nome da salaPara de ignorar um/uma usuário(a), mostrando as mensagens dele/dela de agora em dianteIgnora um/uma usuário(a), escondendo as mensagens dele/dela de você
- Indisponível
+ ForaOfflineOnlineEscolher servidorcasa
@@ -2333,7 +2329,7 @@
Abrir câmeraSeu sistema vai automaticamente enviar logs quando um erro incapaz de decriptar ocorreAuro Reportar Erros de Decriptação.
- Sobrepor cor de nick
+ Sobrepor cor de nome de exibiçãoEu já tenho uma contaMensageria segura.Você está em controle.
@@ -2490,4 +2486,8 @@
\n
\nNote que esta ação vai recomeçar o app e pode levar algum tempo.Requisição de sinc inicial
+ Mostrar a info de perfil mais recente (avatar e nome de exibição) para todas as mensagens.
+ Mostrar info de usuária(o) mais recente
+ Ocupada(o)
+ Backup tem uma assinatura válida desta(e) usuária(o).
diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml
index cbc4bea123..5d8a8a7c10 100644
--- a/vector/src/main/res/values-ru/strings.xml
+++ b/vector/src/main/res/values-ru/strings.xml
@@ -1,6 +1,6 @@
- %s приглашение
+ приглашение %s%1$s пригласил(а) %2$s%1$s пригласил(а) вас%1$s вошёл(ла) в комнату
@@ -46,7 +46,7 @@
Начальная синхронизация:
\nИмпорт комнатНачальная синхронизация:
-\nЗагрузка ваших бесед
+\nВаши беседы загружаются
\nЕсли вы присоединились к большому количеству комнат, это может занять некоторое времяНачальная синхронизация:
\nИмпорт приглашенных комнат
@@ -417,9 +417,7 @@
Новый парольНе удалось обновить парольПароль был обновлен
- Отображать сообщения пользователя %s\?
-\n
-\nУчтите, что это действие перезапустит приложение и может занять некоторое время.
+ Показать все сообщения %s\?Выберите странуТемаДоступ к истории комнаты
@@ -531,7 +529,7 @@
Вас заблокировал %2$s в %1$sПричина: %1$sВстряхните устройство, чтобы сообщить об ошибке
- Список участников
+ Участники%d комната%d комнаты
@@ -610,8 +608,8 @@
Изменить ваше отображаемое имяВкл/выкл markdownЭта комната была заменена и больше не активна.
- Этот разговор продолжается здесь
- Эта комната является продолжением другого разговора
+ Беседа продолжается здесь
+ Эта комната — продолжение другой беседыНажмите здесь для просмотра старых сообщенийПрисоединиться к комнате с указанным адресомДля исправления управления приложениями Matrix
@@ -901,7 +899,7 @@
Вы в теме!У вас больше нет непрочитанных сообщенийБеседы
- Здесь будут отображаться ваши диалоги. Нажмите + внизу справа, чтобы начать что-то.
+ Здесь будут ваши диалоги. Нажмите кнопку «+» внизу справа, чтобы начать беседу.КомнатыЗдесь будут отображаться ваши комнаты. Коснитесь + внизу справа, чтобы найти существующие или создать свои.Реакции
@@ -961,7 +959,7 @@
(изменено)Изменение сообщенияИзменения не найдены
- Отфильтровывать разговоры…
+ Отфильтровать беседы…Не можете найти то, что ищете\?Создать новую комнатуОтправить новое сообщение в диалог
@@ -1073,7 +1071,7 @@
Введите ключевые слова, чтобы найти реакцию.Долгий клик по комнате покажет дополнительные опцииНепрочитанные сообщения
- Это ваш разговор. Владейте им.
+ Это ваша переписка. Контролируйте её.Общайтесь с людьми напрямую или в группахНачнемВыберите сервер
@@ -1307,7 +1305,7 @@
\n
\nВы можете отменить это действие в любое время в общих настройках.Перестать игнорировать пользователя
- Если вы перестанете игнорировать этого пользователя, то все его сообщения вновь будут отображаться.
+ Если вы перестанете игнорировать этого пользователя, то вы увидите все его сообщения снова.Отменить приглашениеВы уверены, что хотите отменить приглашение для этого пользователя\?Выгнать пользователя
@@ -1354,7 +1352,7 @@
Вы не игнорируете никаких пользователейВы сделали комнату доступной для всех, у кого есть ссылка.Вы сделали комнату только по приглашению.
- Сохраняйте конфиденциальность разговоров с помощью шифрования
+ Храните тайну переписки с помощью шифрованияРасширьте и персонализируйте свой опыт использованияПрисоединяйтесь к миллионам бесплатно на крупнейшем публичном сервереВойти в %1$s
@@ -1824,7 +1822,7 @@
Добавить людей"Тема: "Добавьте тему
- Это начало разговора.
+ Это начало беседы.Это начало %s.У вас нет разрешения на включение шифрования в этой комнате.Создание комнаты…
@@ -2264,10 +2262,6 @@
Добавьте пространство в любое пространство, которым вы управляете.Добавить существующие пространстваДобавить существующие комнаты
- Выберите, что нужно покинуть
- Покинуть определенные комнаты и пространства…
- Не покидать ни одну комнату и пространство
- Покинуть все комнаты и пространстваВы уверены, что хотите покинуть %s\?Обнаружение (%s)Завершение настройки
@@ -2404,11 +2398,11 @@
Создать учетную записьОбмен сообщениями для вашей команды.Сквозное шифрование не требующее номера телефона. Нет рекламы или сбора данных.
- Выбор где хранятся ваши разговоры дает вам власть и независимость. Подключено с помощью Matrix.
+ Решайте самостоятельно и независимо, где хранить беседы. Благодаря Matrix.Безопасное и независимое общение, обеспечивающее вам такой же уровень конфиденциальности, как при личном общении в вашем собственном доме.Безопасный обмен сообщениями.Ваше управление.
- Ваши собственные разговоры.
+ Контролируйте свою переписку.МестоположениеВы согласны отправить эту информацию\?Чтобы обнаружить существующие контакты, необходимо отправить контактную информацию (электронную почту и номера телефонов) на сервер обнаружения. Мы хешируем ваши данные перед отправкой для обеспечения конфиденциальности.
@@ -2441,7 +2435,7 @@
Не могу связаться с домашним сервером на URL %s. Пожалуйста, проверьте вашу ссылку или выберите домашний сервер вручную.Не сейчасВключить
- Поиск уведомлений
+ Ожидание уведомленийВам не разрешено подключаться к этой комнатеОрганизуйте обсуждение с помощью ветокПоказать все ветки, в которых вы участвуете
@@ -2468,7 +2462,7 @@
Команда «%s» распознается, но не поддерживается в ветках.Из веткиСовет: нажмите и удерживайте сообщение и используйте «%s».
- Ветки помогают хранить ваши разговоры по темам и легко отслеживать их.
+ Ветки помогают не терять нить беседы, и за ними легко следить.Мои веткиПоказать все ветки этой комнатыФильтр
@@ -2494,4 +2488,32 @@
%d изменений ACL сервера%d изменений ACL сервера
+ Поехали!
+ Его можно изменить позже
+ Добавить аватар
+ Его можно изменить позже
+ Имя для показа
+ Показывается, когда вы отправляете сообщения.
+ Выберите имя для показа
+ Ваш аккаунт «%s» создан.
+ Поздравляем!
+ Домой
+ Персонализировать
+ БЕТА
+ Отзывы на бета-версию веток
+ Оставить отзыв
+ БЕТА
+ Если включено, для других вы всегда будете выглядеть, как в офлайне, даже при использовании приложения.
+ Офлайн-режим
+ Бета-версия веток
+ Бета-версия веток
+ Ветки почти в бете 🎉
+ Остановить демонстрацию экрана
+ Демонстрировать экран
+ Узнать больше
+ Попробуйте
+ Отключить
+ - Несколько пользователей больше не игнорируются
+ Начальный запрос на синхронизацию
+ Ветки помогают не терять нить беседы, и за ними легко следить.%sВключение веток обновит приложение. Может занять продолжительное время.
diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml
index a7a9f152fd..05562b7b4b 100644
--- a/vector/src/main/res/values-sk/strings.xml
+++ b/vector/src/main/res/values-sk/strings.xml
@@ -1752,7 +1752,6 @@
Pridať priestor do priestoru, ktorý spravujete.Pridať existujúce miestnostiPridať existujúce miestnosti a priestor
- Neopustiť žiadne miestnosti ani priestorySte jediným správcom tohto priestoru. Jeho opustenie bude znamenať, že nad ním nikto nebude mať kontrolu.Ste tu jediný človek. Ak odídete, nikto sa už v budúcnosti nebude môcť pripojiť do tejto miestnosti, vrátane vás.Ste si istí, že chcete opustiť %s\?
@@ -1882,8 +1881,6 @@
Spravovať miestnosti a priestoryPriestory sú novým spôsobom spájania miestností a ľudí.Pridať existujúce priestory
- Opustiť konkrétne miestnosti a priestory…
- Opustiť všetky miestnosti a priestoryVytvorme pre každého z nich miestnosť. Neskôr môžete pridať aj ďalšie, vrátane už existujúcich.Na akých veciach pracujete\?Zabezpečte, aby mali k spoločnosti %s prístup správni ľudia. Neskôr môžete pozvať ďalších.
@@ -2385,7 +2382,7 @@
Hľadať názovSpätnú väzbu sa nepodarilo odoslať (%s)Zobraziť informácie o ladení chýb na obrazovke
- Nahradiť farbu prezývky
+ Nahradiť farbu zobrazovaného menaChcete sa pripojiť k existujúcemu serveru\?Udalosť bola upravená administrátorom miestnosti, dôvod: %1$sTáto miestnosť je momentálne neprístupná.
@@ -2457,7 +2454,6 @@
Definovanie predvoleného kľúča SSSSRýchle-zlyhanieNajprv konzultovať
- Vyberte si, čo opustíteOpustiť miestnosť s daným id (alebo aktuálnu miestnosť, ak je prázdna)Varovná úroveň dôveryhodnostiPripojiť
@@ -2537,4 +2533,8 @@
\n
\nUpozorňujeme, že táto akcia spôsobí reštart aplikácie a môže chvíľu trvať.Úvodná žiadosť o synchronizáciu
+ Zobraziť najnovšie informácie o profile (obrázok a zobrazované meno) pre všetky správy.
+ Zobraziť najnovšie informácie o používateľovi
+ Obsadený/zaneprázdnený
+ Zálohovanie má platný podpis od tohto používateľa.
diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml
index 669cfa6b68..103afebed7 100644
--- a/vector/src/main/res/values-sq/strings.xml
+++ b/vector/src/main/res/values-sq/strings.xml
@@ -2190,10 +2190,6 @@
Shtoni një hapësirë te cilado hapësirë që administroni.Shtoni hapësira ekzistueseShtoni dhoma ekzistuese
- Zgjidhni gjëra që duhen braktisur
- Braktisni dhoma dhe hapësira të përcaktuara…
- Mos braktis ndonjë dhomë dhe hapësirë
- Braktis krejt dhomat dhe hapësiratJeni i sigurt se doni të dilni nga \'%s\?Zbulim (%s)Përfundoje ujdisjen
diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml
index 5a05f73cf4..9f54bc2c33 100644
--- a/vector/src/main/res/values-sv/strings.xml
+++ b/vector/src/main/res/values-sv/strings.xml
@@ -2239,10 +2239,6 @@
Lägg till ett utrymme till alla utrymmen du hanterar.Lägg till befintliga utrymmenLägg till befintliga rum
- Välj saker att lämna
- Lämna specifika rum och utrymmen…
- Lämna inga rum och utrymmen
- Lämna alla rum och utrymmenÄr du säker på att du vill lämna %s\?Upptäckt (%s)Slutför konfigurationen
@@ -2251,7 +2247,7 @@
Slutför inställning av upptäckbarhet.Du använder för närvarande ingen identitetsserver. För att bjuda in teammedlemmar och kunna upptäckas av dem, konfigurera en nedan.Bjud in med användarnamn eller e-postadress
- Otillgänglig
+ BortaOfflineOnlineVälj hemserver
@@ -2490,4 +2486,8 @@
\n
\nObservera att detta startar om appen, och kan ta ett tag.Förfrågan om inledande synk
+ Visa den senaste profilinfon (avatar och visningsnamn) för alla meddelandena.
+ Visa senaste användarinfon
+ Upptagen
+ Säkerhetskopian har en giltig signatur från den här användaren.
diff --git a/vector/src/main/res/values-ta/strings.xml b/vector/src/main/res/values-ta/strings.xml
index 26f337757f..5150c3e87b 100644
--- a/vector/src/main/res/values-ta/strings.xml
+++ b/vector/src/main/res/values-ta/strings.xml
@@ -1,4 +1,81 @@
- %1$s அறையில் சேர்ந்தார்
+ %1$s அறையில் சேர்ந்துள்ளார்
+ • IP எழுத்துக்கள் உடன் பொருந்தக்கூடிய சேவையகங்கள் இப்போது தடைசெய்யப்பட்டுள்ளன.
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் இப்போது தடைசெய்யப்பட்டுள்ளன.
+
+ %d சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் மாற்றம்
+ %d சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் மாற்றங்கள்
+
+ நீங்கள் இந்த அறைக்கான சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் (ACL) களை மாற்றினீர்கள்.
+ %s இந்த அறைக்கான சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் (ACL) களை மாற்றினார்.
+ • IP எழுத்துக்கள் உடன் பொருந்தக்கூடிய சேவையகங்கள் தடைசெய்யப்பட்டுள்ளன.
+ • IP எழுத்துக்கள் உடன் பொருந்தக்கூடிய சேவையகங்கள் அனுமதிக்கப்பட்டுள்ளன.
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் அனுமதிக்கப்பட்டுள்ளன.
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் தடைசெய்யப்பட்டுள்ளன.
+ நீங்கள் இந்த அறைக்கான சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் (ACL) களை அமைத்தீர்கள்.
+ %s இந்த அறைக்கான சேவையக-அணுகல் கட்டுப்பாட்டுப் பட்டியல் (ACL) களை அமைத்தார்.
+ %s இங்குத் திறமுயர்த்தினார்.
+ நீங்கள் இங்குத் திறமுயர்த்தினீர்கள்.
+ நீங்கள் இந்த அறையைத் திறமுயர்த்தினீர்கள்.
+ %s இந்த அறையைத் திறமுயர்த்தினார்.
+ யாரேனும் ஒருவர்.
+ எல்லா அறை உறுப்பினர்களும்.
+ எல்லா அறை உறுப்பினர்களும், அவர்கள் இணைந்த இடத்திலிருந்து.
+ எல்லா அறை உறுப்பினர்களும், அவர்கள் அழைக்கப்பட்ட இடத்திலிருந்து.
+ அழைப்பை அமைக்க %s தரவை அனுப்பியுள்ளார்.
+ %1$s என்பவர் வருங்கால குறுஞ்செய்திகள் %2$s க்கு புலப்படும்படி அமைத்தார்
+ நீங்கள் வருங்கால குறுஞ்செய்திகள் %1$s க்கு புலப்படும்படி அமைத்தீர்கள்
+ நீங்கள் வருங்கால அறை வரலாறு %1$s க்கு புலப்படும்படி அமைத்தீர்கள்
+ %1$s என்பவர் வருங்கால அறை வரலாறு %2$s க்கு புலப்படும்படி அமைத்தார்
+ நீங்கள் அழைப்பை முடித்தீர்கள்.
+ %s அழைப்பை முடித்தார்.
+ நீங்கள் அழைப்புக்குப் பதிலளித்தீர்கள்.
+ %s அழைப்புக்கு பதிலளித்தார்.
+ அழைப்பை அமைக்க நீங்கள் தரவை அனுப்பியுள்ளீர்கள்.
+ நீங்கள் குரல் அழைப்பு விடுத்துள்ளீர்.
+ %s என்பவர் குரல் அழைப்பு விடுத்துள்ளார்.
+ நீங்கள் காணொளி அழைப்பு விடுத்துள்ளீர்.
+ %s என்பவர் காணொளி அழைப்பு விடுத்துள்ளார்.
+ %1$s என்பவர் அறை பெயரை %2$s என மாற்றியுள்ளார்
+ நீங்கள் அறை பெயரை %1$s என மாற்றியுள்ளீர்கள்
+ நீங்கள் அறை படத்தை மாற்றியுள்ளீர்கள்
+ %1$s என்பவர் தலைப்பை %2$s க்கு மாற்றினார்
+ %1$s என்பவர் அறை படத்தை மாற்றியுள்ளார்
+ நீங்கள் தலைப்பை %1$s க்கு மாற்றியுள்ளீர்கள்
+ நீங்கள் உங்கள் காட்சிப் பெயரை நீக்கியுள்ளீர்கள் (முன்பு %1$s ஆக இருந்தது)
+ %1$s தனது காட்சிப் பெயரை நீக்கியுள்ளார் (முன்பு %2$s ஆக இருந்தது)
+ நீங்கள் உங்கள் காட்சிப் பெயரை %1$s இருந்து %2$s க்கு மாற்றியுள்ளீர்கள்
+ %1$s தனது காட்சிப் பெயரை %2$s இருந்து %3$s க்கு மாற்றினார்
+ %1$s தனது காட்சிப் பெயரை %2$s என அமைத்தார்
+ நீங்கள் உங்கள் காட்சிப் பெயரை %1$s என அமைத்துள்ளீர்கள்
+ நீங்கள் உங்கள் சுயவிவரப் படத்தை மாற்றினீர்கள்
+ %1$s தனது சுயவிவரப் படத்தை மாற்றினார்
+ %2$s இன் அழைப்பிதழை %1$s திரும்பப் பெற்றார்
+ %1$s இன் அழைப்பிதழை நீங்கள் திரும்பப் பெற்றீர்கள்
+ நீங்கள் %1$s ஐ தடை செய்துள்ளீர்கள்
+ %1$s என்பவர் %2$s ஐ தடை செய்துள்ளார்
+ நீங்கள் %1$s இன் தடையை நீக்கியுள்ளீர்கள்
+ %1$s என்பவர் %2$s இன் தடையை நீக்கியுள்ளார்
+ %2$s ஐ %1$s நீக்கினார்
+ %1$s ஐ நீங்கள் நீக்கினீர்கள்
+ நீங்கள் அறையை விட்டு வெளியேறினீர்கள்
+ %1$s அறையை விட்டு வெளியேறினார்
+ நீங்கள் அறையை விட்டு வெளியேறினீர்கள்
+ %1$s அறையை விட்டு வெளியேறினார்
+ நீங்கள் சேர்ந்துள்ளீர்கள்
+ %1$s சேர்ந்துள்ளார்
+ நீங்கள் அறையில் சேர்ந்துள்ளீர்கள்
+ %1$s உங்களை அழைத்துள்ளார்
+ நீங்கள் %1$s ஐ அழைத்துள்ளீர்கள்
+ %1$s என்பவர் %2$s ஐ அழைத்துள்ளார்
+ நீங்கள் கலந்துரையாடலை உருவாக்கியுள்ளீர்கள்
+ %1$s கலந்துரையாடலை உருவாக்கியுள்ளார்
+ நீங்கள் இந்த அறையை உருவாக்கியுள்ளீர்கள்
+ %1$s அறையை உருவாக்கியுள்ளார்
+ உங்கள் அழைப்பிதழ்
+ %s இன் அழைப்பிதழ்
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் அனுமதிக்கப்பட்ட பட்டியலில் இருந்து நீக்கப்பட்டுள்ளது.
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் இப்போது அனுமதிக்கப்பட்டுள்ளன.
+ • %s உடன் பொருந்தக்கூடிய சேவையகங்கள் தடை பட்டியலில் இருந்து நீக்கப்பட்டுள்ளது.
\ No newline at end of file
diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml
index d9847bbb81..ea982f906b 100644
--- a/vector/src/main/res/values-uk/strings.xml
+++ b/vector/src/main/res/values-uk/strings.xml
@@ -2133,7 +2133,6 @@
Додайте простір до будь-якого простору, яким ви керуєте.Додати наявні просториДодати наявні кімнати
- Вийти з усіх кімнат і просторівВи впевнені, що хочете вийти з %s\?Приватний простір для вас і учасників вашої командиЯ та учасники команди
@@ -2155,7 +2154,7 @@
Надіслати подію стануНадіслати власну подіюДізнатися стан кімнати
- Недоступний
+ ВідсутнійНе в мережіУ мережіПерегляд підтвердження прочитання
@@ -2197,9 +2196,6 @@
Зверніться до адміністратора домашнього сервера за подробицямиСхоже, ваш домашній сервер ще не підтримує просторівШукаєте когось не в %s\?
- Виберіть, звідки хочете вийти
- Вийти з обраних кімнат і просторів…
- Не виходити з жодної кімнати й просторуЗавершити налаштуванняНа разі пропуститиПриєднуйтеся до мого простору %1$s %2$s
@@ -2421,7 +2417,7 @@
Відкрити камеруВаша система автоматично надсилатиме журнали, коли виникне помилка неможливості розшифруванняАвтозвіт про помилки шифрування.
- Замінити колір псевдоніма
+ Замінити колір показуваного іменіЯ вже маю обліковий записЗахищене спілкування.Ви контролюєте все.
@@ -2582,4 +2578,8 @@
\n
\nЗауважте, що ця дія перезапустить застосунок, і це може тривати деякий час.Початковий запит синхронізації
+ Показати найновіші відомості про профіль (аватар і показуване ім\'я) для всіх повідомлень.
+ Показати найновіші відомості про користувача
+ Зайнятий
+ Резервна копія має дійсний підпис від цього користувача.
diff --git a/vector/src/main/res/values-vi/strings.xml b/vector/src/main/res/values-vi/strings.xml
index 1ca481e798..c377ae49e3 100644
--- a/vector/src/main/res/values-vi/strings.xml
+++ b/vector/src/main/res/values-vi/strings.xml
@@ -1270,10 +1270,6 @@
Thêm Space hiện cóThêm các phòng hiện cóThêm các phòng và Space hiện có
- Chọn những thứ để rời khỏi
- Để lại các phòng và Space cụ thể…
- Không rời bất kỳ phòng và Space nào
- Rời khỏi tất cả phòng và SpaceBạn là quản trị viên duy nhất của không gian này. Rời khỏi nó sẽ có nghĩa là không ai có quyền kiểm soát nó.Bạn sẽ không thể tham gia lại trừ khi bạn được mời lại.Bạn là người duy nhất ở đây. Nếu bạn rời đi, sẽ không ai có thể tham gia trong tương lai, kể cả bạn
diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml
index 0130c20879..36292ff61d 100644
--- a/vector/src/main/res/values-zh-rCN/strings.xml
+++ b/vector/src/main/res/values-zh-rCN/strings.xml
@@ -2162,10 +2162,6 @@
将一个空间添加到您管理的任何空间。添加现有空间添加现有聊天室
- 选择要离开的项目
- 离开特定聊天室和空间…
- 不要离开任何聊天室和空间
- 离开所有聊天室和空间你确定要离开 %s 吗?发现 (%s)完成设置
diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml
index 8fd51266f5..3ce2d9c595 100644
--- a/vector/src/main/res/values-zh-rTW/strings.xml
+++ b/vector/src/main/res/values-zh-rTW/strings.xml
@@ -2158,10 +2158,6 @@
新增空間至您管理的任何空間。新增既有的空間新增既有的聊天室
- 挑選要離開的對象
- 離開特定的聊天室與空間……
- 不要離開任何聊天室與空間
- 離開所有聊天室與空間您確定您想要離開 %s?探索 (%s)結束設定
@@ -2211,7 +2207,7 @@
設定聊天室名稱停止忽略使用者,繼續顯示他們的訊息忽略使用者,為您隱藏他們的訊息
- 不可用
+ 離開離線線上選擇家伺服器
@@ -2288,7 +2284,7 @@
開啟攝影機發生無法解密錯誤時,您的系統將會自動傳送紀錄檔自動回報解密錯誤。
- 覆寫暱稱色彩
+ 覆寫顯示名稱色彩我已有一個帳號安全傳送訊息。您已掌控了您的資料。
@@ -2443,4 +2439,8 @@
\n
\n注意,此動作將會重新啟動應用程式,並可能需要一些時間。初始同步請求
+ 顯示所有訊息的最新個人檔案資訊(大頭照與顯示名稱)。
+ 顯示最新的使用者資訊
+ 忙碌
+ 備份具有來自該使用者的有效簽名。
diff --git a/vector/src/main/res/values/donottranslate.xml b/vector/src/main/res/values/donottranslate.xml
index 2dcd36d7c8..eb8efa0ad2 100755
--- a/vector/src/main/res/values/donottranslate.xml
+++ b/vector/src/main/res/values/donottranslate.xml
@@ -19,6 +19,8 @@
Join millions for free on the largest public serverEdit
+ Welcome back!
+
Choose your serverWhat is the address of your server? Server is like a home for all your data.Server URL
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 63d4730dc5..788dee1bc3 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -973,6 +973,8 @@
Click on the read receipts for a detailed list.Show chat effectsUse /confetti command or send a message containing ❄️ or 🎉
+ Autoplay animated images
+ Play animated images in the timeline as soon as they are visibleShow join and leave eventsInvites, removes, and bans are unaffected.Show account events
@@ -1422,7 +1424,7 @@
collapse%1$s: %2$s
- %d+
+ +%dNo valid Google Play Services APK found. Notifications may not work properly.
@@ -1584,9 +1586,11 @@
You’re all caught up!You have no more unread messagesConversations
- Your direct message conversations will be displayed here. Tap the + bottom right to start some.
+
+ Your direct message conversations will be displayed here. Tap the + at the bottom right to start some.Rooms
- Your rooms will be displayed here. Tap the + bottom right to find existing ones or start some of your own.
+
+ Your rooms will be displayed here. Tap the + at the bottom right to find existing ones or start some of your own.ReactionsAdd Reaction
@@ -2847,15 +2851,6 @@
You won\'t be able to rejoin unless you are re-invited.You\'re the only admin of this space. Leaving it will mean no one has control over it.
-
- Leave all rooms and spaces
-
- Don’t leave any rooms and spaces
-
- Leave specific rooms and spaces…
-
- Pick things to leave
-
No results foundSome results may be hidden because they’re private and you need an invite to them.
@@ -3015,8 +3010,10 @@
15 minutes1 hour8 hours
- Allow access
- If you’d like to share your Live location, ${app_name} needs location access all the time when the app is in the background.\nWe will only access your location for the duration that you choose.
+
+ Allow access
+
+ If you’d like to share your Live location, ${app_name} needs location access all the time when the app is in the background.\nWe will only access your location for the duration that you choose.${app_name} could not access your location${app_name} could not access your location. Please try again later.Open with
@@ -3037,6 +3034,8 @@
Location sharing is in progressEnable Live Location SharingTemporary implementation: locations persist in room history
+ Stop sharing
+ Updated %1$s agoShow Message bubbles
diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml
index 7ff588ca76..1a3c62e0e8 100644
--- a/vector/src/main/res/xml/vector_settings_preferences.xml
+++ b/vector/src/main/res/xml/vector_settings_preferences.xml
@@ -140,6 +140,12 @@
android:summary="@string/settings_chat_effects_description"
android:title="@string/settings_chat_effects_title" />
+
+
()
+
+ private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper)
+
+ @Before
+ fun setUp() {
+ mockkStatic("androidx.lifecycle.FlowLiveDataConversions")
+ }
+
+ @After
+ fun tearDown() {
+ unmockkStatic("androidx.lifecycle.FlowLiveDataConversions")
+ }
+
+ @Test
+ fun `given a room id then the correct flow of view states list is collected`() = runTest {
+ val roomId = "roomId"
+
+ val summary1 = LiveLocationShareAggregatedSummary(
+ userId = "userId1",
+ isActive = true,
+ endOfLiveTimestampMillis = 123,
+ lastLocationDataContent = MessageBeaconLocationDataContent()
+ )
+ val summary2 = LiveLocationShareAggregatedSummary(
+ userId = "userId2",
+ isActive = true,
+ endOfLiveTimestampMillis = 1234,
+ lastLocationDataContent = MessageBeaconLocationDataContent()
+ )
+ val summary3 = LiveLocationShareAggregatedSummary(
+ userId = "userId3",
+ isActive = true,
+ endOfLiveTimestampMillis = 1234,
+ lastLocationDataContent = MessageBeaconLocationDataContent()
+ )
+ val summaries = listOf(summary1, summary2, summary3)
+ val liveData = fakeSession.roomService()
+ .getRoom(roomId)
+ .locationSharingService()
+ .givenRunningLiveLocationShareSummaries(summaries)
+
+ every { liveData.asFlow() } returns flowOf(summaries)
+
+ val viewState1 = UserLiveLocationViewState(
+ matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
+ pinDrawable = mockk(),
+ locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
+ endOfLiveTimestampMillis = 123,
+ locationTimestampMillis = 123,
+ showStopSharingButton = false
+ )
+ val viewState2 = UserLiveLocationViewState(
+ matrixItem = MatrixItem.UserItem(id = "@userId2:matrix.org", displayName = "User 2", avatarUrl = ""),
+ pinDrawable = mockk(),
+ locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
+ endOfLiveTimestampMillis = 1234,
+ locationTimestampMillis = 1234,
+ showStopSharingButton = false
+ )
+ coEvery { viewStateMapper.map(summary1) } returns viewState1
+ coEvery { viewStateMapper.map(summary2) } returns viewState2
+ coEvery { viewStateMapper.map(summary3) } returns null
+
+ val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first()
+
+ assertEquals(listOf(viewState1, viewState2), viewStates)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt
new file mode 100644
index 0000000000..b477265506
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.live.map
+
+import com.airbnb.mvrx.test.MvRxTestRule
+import im.vector.app.features.location.LocationData
+import im.vector.app.features.location.LocationSharingServiceConnection
+import im.vector.app.test.test
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.verify
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.api.util.MatrixItem
+
+class LocationLiveMapViewModelTest {
+
+ @get:Rule
+ val mvrxTestRule = MvRxTestRule()
+
+ private val fakeRoomId = ""
+
+ private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
+
+ private val getListOfUserLiveLocationUseCase = mockk()
+ private val locationServiceConnection = mockk()
+
+ private fun createViewModel(): LocationLiveMapViewModel {
+ return LocationLiveMapViewModel(
+ LocationLiveMapViewState(args),
+ getListOfUserLiveLocationUseCase,
+ locationServiceConnection
+ )
+ }
+
+ @Test
+ fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
+ val userLocations = listOf(
+ UserLiveLocationViewState(
+ MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
+ pinDrawable = mockk(),
+ locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
+ endOfLiveTimestampMillis = 123,
+ locationTimestampMillis = 123,
+ showStopSharingButton = false
+ )
+ )
+ every { locationServiceConnection.bind(any()) } just runs
+ every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations)
+
+ val viewModel = createViewModel()
+ viewModel
+ .test()
+ .assertState(
+ LocationLiveMapViewState(args).copy(
+ userLocations = userLocations
+ )
+ )
+ .finish()
+
+ verify { locationServiceConnection.bind(viewModel) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt
new file mode 100644
index 0000000000..95e0ff1b0b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.live.map
+
+import android.graphics.drawable.Drawable
+import im.vector.app.features.location.LocationData
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeLocationPinProvider
+import im.vector.app.test.fakes.FakeSession
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.session.getUser
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
+import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.api.util.toMatrixItem
+
+private const val A_USER_ID = "@aUserId:matrix.org"
+private const val A_USER_DISPLAY_NAME = "A_USER_DISPLAY_NAME"
+private const val A_IS_ACTIVE = true
+private const val A_END_OF_LIVE_TIMESTAMP = 123L
+private const val A_LOCATION_TIMESTAMP = 122L
+private const val A_LATITUDE = 40.05
+private const val A_LONGITUDE = 29.24
+private const val A_UNCERTAINTY = 30.0
+private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY"
+
+class UserLiveLocationViewStateMapperTest {
+
+ private val locationPinProvider = FakeLocationPinProvider()
+ private val fakeSession = FakeSession()
+ private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
+
+ private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance, fakeActiveSessionHolder.instance)
+
+ @Before
+ fun setup() {
+ mockkStatic("org.matrix.android.sdk.api.util.MatrixItemKt")
+ val fakeUser = mockk()
+ every { fakeSession.getUser(A_USER_ID) } returns fakeUser
+ every { fakeUser.toMatrixItem() } returns MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = "")
+ }
+
+ @After
+ fun tearDown() {
+ unmockkStatic("org.matrix.android.sdk.api.util.MatrixItemKt")
+ }
+
+ @Test
+ fun `given a summary with invalid data then result is null`() = runTest {
+ val summary1 = LiveLocationShareAggregatedSummary(
+ userId = null,
+ isActive = true,
+ endOfLiveTimestampMillis = null,
+ lastLocationDataContent = null,
+ )
+ val summary2 = summary1.copy(userId = "")
+ val summaryWithoutLocation = summary1.copy(userId = A_USER_ID)
+
+ val viewState1 = userLiveLocationViewStateMapper.map(summary1)
+ val viewState2 = userLiveLocationViewStateMapper.map(summary2)
+ val viewState3 = userLiveLocationViewStateMapper.map(summaryWithoutLocation)
+
+ viewState1 shouldBeEqualTo null
+ viewState2 shouldBeEqualTo null
+ viewState3 shouldBeEqualTo null
+ }
+
+ @Test
+ fun `given a summary with valid data then result is correctly mapped`() = runTest {
+ val pinDrawable = mockk()
+
+ val locationDataContent = MessageBeaconLocationDataContent(
+ locationInfo = LocationInfo(geoUri = A_GEO_URI),
+ unstableTimestampMillis = A_LOCATION_TIMESTAMP
+ )
+ val summary = LiveLocationShareAggregatedSummary(
+ userId = A_USER_ID,
+ isActive = A_IS_ACTIVE,
+ endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP,
+ lastLocationDataContent = locationDataContent,
+ )
+ locationPinProvider.givenCreateForUserId(A_USER_ID, pinDrawable)
+
+ val viewState = userLiveLocationViewStateMapper.map(summary)
+
+ val expectedViewState = UserLiveLocationViewState(
+ matrixItem = MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = ""),
+ pinDrawable = pinDrawable,
+ locationData = LocationData(
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = A_UNCERTAINTY
+ ),
+ endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP,
+ locationTimestampMillis = A_LOCATION_TIMESTAMP,
+ showStopSharingButton = false
+ )
+ viewState shouldBeEqualTo expectedViewState
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt
index 1fed03b601..d3600940ab 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt
@@ -32,13 +32,13 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.WellKnown
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
-private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
+private val A_DIRECT_LOGIN_ACTION = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown())
private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown())
private val A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT = WellknownResult.FailPrompt(null, null)
private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null
private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig(
- homeServerUri = FakeUri("https://${A_LOGIN_OR_REGISTER_ACTION.username.getServerName()}").instance,
+ homeServerUri = FakeUri("https://${A_DIRECT_LOGIN_ACTION.matrixId.getServerName()}").instance,
homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance,
identityServerUri = null
)
@@ -54,11 +54,11 @@ class DirectLoginUseCaseTest {
@Test
fun `when logging in directly, then returns success with direct session result`() = runTest {
- fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
- val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
+ fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
+ val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
- val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
+ val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.success(fakeSession)
}
@@ -66,14 +66,14 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest {
fakeAuthenticationService.givenWellKnown(
- A_LOGIN_OR_REGISTER_ACTION.username,
+ A_DIRECT_LOGIN_ACTION.matrixId,
config = NO_HOMESERVER_CONFIG,
result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT
)
- val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
+ val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
- val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
+ val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.success(fakeSession)
}
@@ -81,14 +81,14 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest {
fakeAuthenticationService.givenWellKnown(
- A_LOGIN_OR_REGISTER_ACTION.username,
+ A_DIRECT_LOGIN_ACTION.matrixId,
config = NO_HOMESERVER_CONFIG,
result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT
)
- val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
+ val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
- val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
+ val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result should { this.isFailure }
result should { this.exceptionOrNull() is Exception }
@@ -97,20 +97,20 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown throws, when logging in directly, then returns failure result with original cause`() = runTest {
- fakeAuthenticationService.givenWellKnownThrows(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
+ fakeAuthenticationService.givenWellKnownThrows(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
- val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
+ val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.failure(AN_ERROR)
}
@Test
fun `given direct authentication throws, when logging in directly, then returns failure result with original cause`() = runTest {
- fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
- val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
+ fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
+ val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthenticationThrows(A_FALLBACK_CONFIG, username, password, initialDeviceName, cause = AN_ERROR)
- val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
+ val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.failure(AN_ERROR)
}
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index ac8a4c364e..1abfa7e9a8 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -18,6 +18,8 @@ package im.vector.app.features.onboarding
import android.net.Uri
import com.airbnb.mvrx.test.MvRxTestRule
+import im.vector.app.R
+import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.SignMode
@@ -38,6 +40,8 @@ import im.vector.app.test.fakes.FakeUri
import im.vector.app.test.fakes.FakeUriFilenameResolver
import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorOverrides
+import im.vector.app.test.fakes.toTestString
+import im.vector.app.test.fixtures.aBuildMeta
import im.vector.app.test.fixtures.aHomeServerCapabilities
import im.vector.app.test.test
import kotlinx.coroutines.test.runTest
@@ -59,7 +63,7 @@ private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT)
-private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
+private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
@@ -142,11 +146,11 @@ class OnboardingViewModelTest {
@Test
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
- fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
+ fakeDirectLoginUseCase.givenSuccessResult(A_DIRECT_LOGIN, config = null, result = fakeSession)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
- viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
+ viewModel.handle(A_DIRECT_LOGIN)
test
.assertStatesChanges(
@@ -161,11 +165,11 @@ class OnboardingViewModelTest {
@Test
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
- fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
+ fakeDirectLoginUseCase.givenFailureResult(A_DIRECT_LOGIN, config = null, cause = AN_ERROR)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
- viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
+ viewModel.handle(A_DIRECT_LOGIN)
test
.assertStatesChanges(
@@ -242,6 +246,27 @@ class OnboardingViewModelTest {
.finish()
}
+ @Test
+ fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
+ fakeContext.givenHasConnection()
+ fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
+ fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG)
+ val test = viewModel.test()
+
+ viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, null)))
+ viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL))
+
+ val expectedRetryAction = OnboardingAction.HomeServerChange.SelectHomeServer("${R.string.matrix_org_server_url.toTestString()}/")
+ test
+ .assertStatesChanges(
+ initialState,
+ { copy(isLoading = true) },
+ { copy(isLoading = false) }
+ )
+ .assertEvents(OnboardingViewEvents.DeeplinkAuthenticationFailure(expectedRetryAction))
+ .finish()
+ }
+
@Test
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
@@ -457,7 +482,8 @@ class OnboardingViewModelTest {
fakeRegisterActionHandler.instance,
fakeDirectLoginUseCase.instance,
fakeStartAuthenticationFlowUseCase.instance,
- FakeVectorOverrides()
+ FakeVectorOverrides(),
+ aBuildMeta()
).also {
viewModel = it
initialState = state
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparatorTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparatorTest.kt
similarity index 92%
rename from vector/src/test/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparatorTest.kt
rename to vector/src/test/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparatorTest.kt
index 010cf5de60..08be0ee058 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparatorTest.kt
@@ -25,7 +25,7 @@ import im.vector.app.test.fixtures.anOtherStage
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
-class FtueMissingRegistrationStagesComparatorTest {
+class MatrixOrgRegistrationStagesComparatorTest {
@Test
fun `when ordering stages, then prioritizes email`() {
@@ -38,7 +38,7 @@ class FtueMissingRegistrationStagesComparatorTest {
aTermsStage()
)
- val result = input.sortedWith(FtueMissingRegistrationStagesComparator())
+ val result = input.sortedWith(MatrixOrgRegistrationStagesComparator())
result shouldBeEqualTo listOf(
anEmailStage(),
diff --git a/vector/src/test/java/im/vector/app/features/widgets/WebviewPermissionUtilsTest.kt b/vector/src/test/java/im/vector/app/features/widgets/WebviewPermissionUtilsTest.kt
new file mode 100644
index 0000000000..927a26b453
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/widgets/WebviewPermissionUtilsTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.widgets
+
+import android.Manifest
+import android.webkit.PermissionRequest
+import im.vector.app.features.widgets.webview.WebviewPermissionUtils
+import im.vector.app.test.fakes.FakeVectorPreferences
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class WebviewPermissionUtilsTest {
+
+ private val prefs = FakeVectorPreferences()
+ private val utils = WebviewPermissionUtils(prefs.instance)
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedAndGrantedNothing() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(),
+ androidPermissionResult = mapOf())
+ permissions shouldBeEqualTo listOf()
+ }
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedNothingGrantedCamera() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(),
+ androidPermissionResult = mapOf(Manifest.permission.CAMERA to true))
+ permissions shouldBeEqualTo listOf()
+ }
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedAndPreviouslyGrantedCamera() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
+ androidPermissionResult = mapOf())
+ permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
+ }
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedAndGrantedCamera() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
+ androidPermissionResult = mapOf(Manifest.permission.CAMERA to true))
+ permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
+ }
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedAndDeniedCamera() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
+ androidPermissionResult = mapOf(Manifest.permission.CAMERA to false))
+ permissions shouldBeEqualTo listOf()
+ }
+
+ @Test
+ fun filterPermissionsToBeGranted_selectedProtectedMediaGrantedNothing() {
+ val permissions = utils.filterPermissionsToBeGranted(
+ selectedWebPermissions = listOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID),
+ androidPermissionResult = mapOf(Manifest.permission.CAMERA to false))
+ permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
index fedda18aeb..0456bbd474 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
@@ -33,7 +33,7 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
}
fun givenRegistrationStarted(started: Boolean) {
- every { isRegistrationStarted } returns started
+ every { isRegistrationStarted() } returns started
}
fun givenLoginFlow(config: HomeServerConnectionConfig, result: LoginFlowResult) {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt
new file mode 100644
index 0000000000..d565105f81
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.test.fakes
+
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeConnectivityManager {
+ val instance = mockk()
+
+ fun givenNoActiveConnection() {
+ every { instance.activeNetwork } returns null
+ }
+
+ fun givenHasActiveConnection() {
+ val network = mockk()
+ every { instance.activeNetwork } returns network
+
+ val networkCapabilities = FakeNetworkCapabilities()
+ networkCapabilities.givenTransports(
+ NetworkCapabilities.TRANSPORT_CELLULAR,
+ NetworkCapabilities.TRANSPORT_WIFI,
+ NetworkCapabilities.TRANSPORT_VPN
+ )
+ every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
index 2a50c34ca3..eb491c9e0c 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
@@ -18,6 +18,7 @@ package im.vector.app.test.fakes
import android.content.ContentResolver
import android.content.Context
+import android.net.ConnectivityManager
import android.net.Uri
import android.os.ParcelFileDescriptor
import io.mockk.every
@@ -48,4 +49,21 @@ class FakeContext(
fun givenMissingSafeOutputStreamFor(uri: Uri) {
every { contentResolver.openOutputStream(uri, "wt") } returns null
}
+
+ fun givenNoConnection() {
+ val connectivityManager = FakeConnectivityManager()
+ connectivityManager.givenNoActiveConnection()
+ givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
+ }
+
+ private fun givenService(name: String, klass: Class, service: T) {
+ every { instance.getSystemService(name) } returns service
+ every { instance.getSystemService(klass) } returns service
+ }
+
+ fun givenHasConnection() {
+ val connectivityManager = FakeConnectivityManager()
+ connectivityManager.givenHasActiveConnection()
+ givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt
index 8a5c6b1cee..289c0a6159 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt
@@ -17,7 +17,7 @@
package im.vector.app.test.fakes
import im.vector.app.features.onboarding.DirectLoginUseCase
-import im.vector.app.features.onboarding.OnboardingAction
+import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -25,11 +25,11 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
class FakeDirectLoginUseCase {
val instance = mockk()
- fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) {
+ fun givenSuccessResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, result: FakeSession) {
coEvery { instance.execute(action, config) } returns Result.success(result)
}
- fun givenFailureResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, cause: Throwable) {
+ fun givenFailureResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, cause: Throwable) {
coEvery { instance.execute(action, config) } returns Result.failure(cause)
}
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt
new file mode 100644
index 0000000000..726093215f
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.test.fakes
+
+import android.graphics.drawable.Drawable
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
+import io.mockk.every
+import io.mockk.invoke
+import io.mockk.mockk
+
+class FakeLocationPinProvider {
+
+ val instance = mockk(relaxed = true)
+
+ fun givenCreateForUserId(userId: String, expectedDrawable: Drawable) {
+ every { instance.create(userId, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt
new file mode 100644
index 0000000000..2cd98c086c
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.room.location.LocationSharingService
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+
+class FakeLocationSharingService : LocationSharingService by mockk() {
+
+ fun givenRunningLiveLocationShareSummaries(summaries: List):
+ LiveData> {
+ return MutableLiveData(summaries).also {
+ every { getRunningLiveLocationShareSummaries() } returns it
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt
new file mode 100644
index 0000000000..36add7128c
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.test.fakes
+
+import android.net.NetworkCapabilities
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeNetworkCapabilities {
+ val instance = mockk()
+
+ fun givenTransports(vararg type: Int) {
+ every { instance.hasTransport(any()) } answers {
+ val input = it.invocation.args.first() as Int
+ type.contains(input)
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt
new file mode 100644
index 0000000000..ff87ab0fde
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.room.Room
+
+class FakeRoom(
+ private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(),
+) : Room by mockk() {
+
+ override fun locationSharingService() = fakeLocationSharingService
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
new file mode 100644
index 0000000000..b09256f747
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.room.RoomService
+
+class FakeRoomService(
+ private val fakeRoom: FakeRoom = FakeRoom()
+) : RoomService by mockk() {
+
+ override fun getRoom(roomId: String) = fakeRoom
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
index 5f02879e65..cf94493f61 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
@@ -33,7 +33,8 @@ class FakeSession(
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
val fakeProfileService: FakeProfileService = FakeProfileService(),
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
- val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
+ val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
+ private val fakeRoomService: FakeRoomService = FakeRoomService(),
) : Session by mockk(relaxed = true) {
init {
@@ -48,6 +49,7 @@ class FakeSession(
override fun profileService(): ProfileService = fakeProfileService
override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService
override fun sharedSecretStorageService() = fakeSharedSecretStorageService
+ override fun roomService() = fakeRoomService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt
index e148432d64..6ec9a4b593 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt
@@ -19,6 +19,7 @@ package im.vector.app.test.fakes
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
+import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.session.securestorage.KeySigner
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
@@ -58,7 +59,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService {
TODO("Not yet implemented")
}
- override suspend fun storeSecret(name: String, secretBase64: String, keys: List) {
+ override suspend fun storeSecret(name: String, secretBase64: String, keys: List) {
TODO("Not yet implemented")
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt
index 697de6bf25..bfbef9e565 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt
@@ -18,6 +18,7 @@ package im.vector.app.test.fakes
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
+import im.vector.app.test.fixtures.aHomeserverUnavailableError
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -29,4 +30,8 @@ class FakeStartAuthenticationFlowUseCase {
fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) {
coEvery { instance.execute(config) } returns result
}
+
+ fun givenHomeserverUnavailable(config: HomeServerConnectionConfig) {
+ coEvery { instance.execute(config) } throws aHomeserverUnavailableError()
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt
new file mode 100644
index 0000000000..b0e6b1dd51
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.test.fixtures
+
+import android.os.Build
+import im.vector.app.core.resources.BuildMeta
+
+fun aBuildMeta() = BuildMeta(Build.VERSION_CODES.O)
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt
index 39c139c208..9ac851ef5e 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt
@@ -18,8 +18,11 @@ package im.vector.app.test.fixtures
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
+import java.net.UnknownHostException
import javax.net.ssl.HttpsURLConnection
fun a401ServerError() = Failure.ServerError(
MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED
)
+
+fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
index 53d38aa228..397ca80f84 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
@@ -77,5 +77,5 @@ fun aNotifiableMessageEvent(
roomIsDirect = false,
canBeReplaced = false,
isRedacted = isRedacted,
- imageUri = null
+ imageUriString = null
)