diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f6a1906394..1c0491fda4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,8 +8,9 @@ on:
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
- -Porg.gradle.jvmargs=-Xmx2g
+ -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
+ --no-daemon
jobs:
debug:
diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml
index 6040fd5f78..a7f1d6f204 100644
--- a/.github/workflows/post-pr.yml
+++ b/.github/workflows/post-pr.yml
@@ -13,6 +13,7 @@ env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
+ --no-daemon
jobs:
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index d0797721e6..d7f5ce8b57 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -9,6 +9,8 @@ on:
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
+ -Porg.gradle.parallel=false
+ --no-daemon
jobs:
check:
@@ -140,7 +142,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Lint analysis
- run: ./gradlew clean :vector:lint --stacktrace
+ run: ./gradlew clean :vector:lint --stacktrace $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v3
@@ -173,7 +175,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Lint ${{ matrix.target }} release
- run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace
+ run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace $CI_GRADLE_ARG_PROPERTIES
- name: Upload ${{ matrix.target }} linting report
if: always()
uses: actions/upload-artifact@v3
@@ -193,7 +195,7 @@ jobs:
- uses: actions/checkout@v3
- name: Run detekt
run: |
- ./gradlew detekt
+ ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v3
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a8a2d91f70..1a9cc5c239 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,8 +8,9 @@ on:
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
- -Porg.gradle.jvmargs=-Xmx2g
+ -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
+ --no-daemon
jobs:
tests:
@@ -49,7 +50,10 @@ jobs:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
emulator-build: 7425822
- script: ./gradlew theCodeCoverageReport -Pandroid.testInstrumentationRunnerArguments.notPackage=im.vector.app.ui --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ script: |
+ ./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ ./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ ./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES
# NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure'
- name: Run all the codecoverage tests at once (retry if emulator failed)
uses: reactivecircus/android-emulator-runner@v2
@@ -62,7 +66,10 @@ jobs:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
emulator-build: 7425822
- script: ./gradlew theCodeCoverageReport -Pandroid.testInstrumentationRunnerArguments.notPackage=im.vector.app.ui --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ script: |
+ ./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ ./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
+ ./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES
- run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
if: always() # we may have failed a previous step and retried, that's OK
env:
diff --git a/.gitignore b/.gitignore
index ff086d7723..8313fb5c63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,4 @@
/fastlane/private
/fastlane/report.xml
-/library/build
+/**/build
diff --git a/CHANGES.md b/CHANGES.md
index e2991a122b..17d3fed2a6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,25 @@
+Changes in Element v1.4.25 (2022-06-27)
+=======================================
+
+Bugfixes 🐛
+----------
+- Second attempt to fix session database migration to version 30.
+
+Changes in Element v1.4.24 (2022-06-22)
+=======================================
+
+Bugfixes 🐛
+----------
+- First attempt to fix session database migration to version 30.
+
+Changes in Element v1.4.23 (2022-06-21)
+=======================================
+
+Bugfixes 🐛
+----------
+ - Fix loop in timeline and simplify management of chunks and timeline events. ([#6318](https://github.com/vector-im/element-android/issues/6318))
+
+
Changes in Element v1.4.22 (2022-06-14)
=======================================
diff --git a/build.gradle b/build.gradle
index 2cb67b7795..0244080ad0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,8 +28,8 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.1.1"
- classpath 'org.owasp:dependency-check-gradle:7.1.0.1'
- classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21"
+ classpath 'org.owasp:dependency-check-gradle:7.1.1'
+ classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.0"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -43,7 +43,7 @@ plugins {
id "io.gitlab.arturbosch.detekt" version "1.20.0"
// Dependency Analysis
- id 'com.autonomousapps.dependency-analysis' version "1.5.0"
+ id 'com.autonomousapps.dependency-analysis' version "1.9.0"
}
// https://github.com/jeremylong/DependencyCheck
@@ -168,7 +168,7 @@ def launchTask = getGradle()
.toString()
.toLowerCase()
-if (launchTask.contains("codeCoverageReport".toLowerCase())) {
+if (launchTask.contains("coverage".toLowerCase())) {
apply from: 'coverage.gradle'
}
@@ -191,7 +191,7 @@ sonarqube {
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization"
property "sonar.java.coveragePlugin", "jacoco"
- property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/theCodeCoverageReport/theCodeCoverageReport.xml"
+ property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/generateCoverageReport/generateCoverageReport.xml"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
}
}
@@ -252,11 +252,7 @@ dependencyAnalysis {
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(":library:ui-styles")
project(":matrix-sdk-android") {
onUnusedDependencies {
exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx
@@ -271,6 +267,8 @@ dependencyAnalysis {
onUnusedDependencies {
// False positives
exclude(
+ "androidx.fragment:fragment-testing",
+ "com.facebook.soloader:soloader",
"com.vanniktech:emoji-google",
"com.vanniktech:emoji-material",
"org.maplibre.gl:android-plugin-annotation-v9",
diff --git a/changelog.d/5821.bugfix b/changelog.d/5821.bugfix
new file mode 100644
index 0000000000..25d8fd2b46
--- /dev/null
+++ b/changelog.d/5821.bugfix
@@ -0,0 +1 @@
+Fixes concurrent modification crash when signing out or launching the app
diff --git a/changelog.d/5864.sdk b/changelog.d/5864.sdk
new file mode 100644
index 0000000000..b0a9d1c67d
--- /dev/null
+++ b/changelog.d/5864.sdk
@@ -0,0 +1 @@
+Group all location sharing related API into LocationSharingService
diff --git a/changelog.d/6101.bugfix b/changelog.d/6101.bugfix
new file mode 100644
index 0000000000..2d8da5327d
--- /dev/null
+++ b/changelog.d/6101.bugfix
@@ -0,0 +1 @@
+Refactor - better naming, return native user id and not sip user id and create a dm with the native user instead of with the sip user.
diff --git a/changelog.d/6155.misc b/changelog.d/6155.misc
new file mode 100644
index 0000000000..044e21408e
--- /dev/null
+++ b/changelog.d/6155.misc
@@ -0,0 +1 @@
+Add unit tests for LiveLocationAggregationProcessor code
diff --git a/changelog.d/6191.sdk b/changelog.d/6191.sdk
new file mode 100644
index 0000000000..d7fcf1b40f
--- /dev/null
+++ b/changelog.d/6191.sdk
@@ -0,0 +1 @@
+Add support for MSC2457 - opting in or out of logging out all devices when changing password
diff --git a/changelog.d/6217.feature b/changelog.d/6217.feature
new file mode 100644
index 0000000000..6a8a31790f
--- /dev/null
+++ b/changelog.d/6217.feature
@@ -0,0 +1 @@
+Improve lock screen implementation.
diff --git a/changelog.d/6315.bugfix b/changelog.d/6315.bugfix
new file mode 100644
index 0000000000..0b5eb6064d
--- /dev/null
+++ b/changelog.d/6315.bugfix
@@ -0,0 +1 @@
+[Location sharing] Fix crash when starting/stopping a live when offline
diff --git a/changelog.d/6318.bugfix b/changelog.d/6318.bugfix
new file mode 100644
index 0000000000..9425c9a258
--- /dev/null
+++ b/changelog.d/6318.bugfix
@@ -0,0 +1 @@
+Fix loop in timeline and simplify management of chunks and timeline events.
diff --git a/changelog.d/6326.bugfix b/changelog.d/6326.bugfix
new file mode 100644
index 0000000000..c09dd8fec1
--- /dev/null
+++ b/changelog.d/6326.bugfix
@@ -0,0 +1 @@
+Update design and behaviour on widget permission bottom sheet
diff --git a/changelog.d/6328.bugfix b/changelog.d/6328.bugfix
new file mode 100644
index 0000000000..7a41996e57
--- /dev/null
+++ b/changelog.d/6328.bugfix
@@ -0,0 +1 @@
+Fix | Some user verification requests couldn't be accepted/declined
diff --git a/changelog.d/6329.misc b/changelog.d/6329.misc
new file mode 100644
index 0000000000..dd87c11f6e
--- /dev/null
+++ b/changelog.d/6329.misc
@@ -0,0 +1 @@
+Fix flaky test in voice recording feature.
diff --git a/changelog.d/6349.bugfix b/changelog.d/6349.bugfix
new file mode 100644
index 0000000000..70718248a7
--- /dev/null
+++ b/changelog.d/6349.bugfix
@@ -0,0 +1 @@
+[Location sharing] Fix stop of a live not possible from another device
diff --git a/changelog.d/6350.feature b/changelog.d/6350.feature
new file mode 100644
index 0000000000..e0bc4ac28b
--- /dev/null
+++ b/changelog.d/6350.feature
@@ -0,0 +1 @@
+Promote live location labs flag
diff --git a/changelog.d/6357.bugfix b/changelog.d/6357.bugfix
new file mode 100644
index 0000000000..231c65030f
--- /dev/null
+++ b/changelog.d/6357.bugfix
@@ -0,0 +1 @@
+Fix backslash escapes in formatted messages
diff --git a/changelog.d/6364.feature b/changelog.d/6364.feature
new file mode 100644
index 0000000000..207d6d141b
--- /dev/null
+++ b/changelog.d/6364.feature
@@ -0,0 +1 @@
+[Location sharing] - Stop any active live before starting a new one
diff --git a/changelog.d/6366.misc b/changelog.d/6366.misc
new file mode 100644
index 0000000000..5752b3d700
--- /dev/null
+++ b/changelog.d/6366.misc
@@ -0,0 +1 @@
+Poll view state unit tests
diff --git a/changelog.d/6369.feature b/changelog.d/6369.feature
new file mode 100644
index 0000000000..3c3e936dfd
--- /dev/null
+++ b/changelog.d/6369.feature
@@ -0,0 +1,2 @@
+ Expose pusher profile tag in advanced settings
+
\ No newline at end of file
diff --git a/changelog.d/6371.bugfix b/changelog.d/6371.bugfix
new file mode 100644
index 0000000000..275ec1cd8f
--- /dev/null
+++ b/changelog.d/6371.bugfix
@@ -0,0 +1 @@
+Fixes wrong error message when signing in with wrong credentials
diff --git a/changelog.d/6375.bugfix b/changelog.d/6375.bugfix
new file mode 100644
index 0000000000..769ed81e69
--- /dev/null
+++ b/changelog.d/6375.bugfix
@@ -0,0 +1 @@
+[Location Share] - Adding missing prefix "u=" for uncertainty in geo URI
diff --git a/changelog.d/6394.misc b/changelog.d/6394.misc
new file mode 100644
index 0000000000..16b4fbf616
--- /dev/null
+++ b/changelog.d/6394.misc
@@ -0,0 +1 @@
+Let LoadRoomMembersTask insert by chunk to release db.
diff --git a/changelog.d/6396.doc b/changelog.d/6396.doc
new file mode 100644
index 0000000000..9b876d74af
--- /dev/null
+++ b/changelog.d/6396.doc
@@ -0,0 +1 @@
+Update the PR process doc to come back to one reviewer with optional additional reviewers.
\ No newline at end of file
diff --git a/coverage.gradle b/coverage.gradle
index fc69ce7e90..f278a475ef 100644
--- a/coverage.gradle
+++ b/coverage.gradle
@@ -24,11 +24,13 @@ def excludes = [
def initializeReport(report, projects, classExcludes) {
projects.each { project -> project.apply plugin: 'jacoco' }
- report.executionData { fileTree(rootProject.rootDir.absolutePath).include(
- "**/build/outputs/unit_test_code_coverage/**/*.exec",
- "**/build/outputs/code_coverage/**/coverage.ec"
- ) }
+ report.executionData {
+ fileTree(rootProject.rootDir.absolutePath).include(
+ "**/build/**/*.exec",
+ "**/build/outputs/code_coverage/**/coverage.ec",
+ )
+ }
report.reports {
xml.enabled true
html.enabled true
@@ -43,13 +45,11 @@ def initializeReport(report, projects, classExcludes) {
switch (project) {
case { project.plugins.hasPlugin("com.android.application") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/gplayDebug")
- androidSourceDirs.add("${project.buildDir}/generated/source/kapt/gplayDebug")
androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java")
break
case { project.plugins.hasPlugin("com.android.library") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug")
- androidSourceDirs.add("${project.buildDir}/generated/source/kapt/debug")
androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java")
break
@@ -70,18 +70,21 @@ def collectProjects(predicate) {
return subprojects.findAll { it.buildFile.isFile() && predicate(it) }
}
-task theCodeCoverageReport(type: JacocoReport) {
+task generateCoverageReport(type: JacocoReport) {
outputs.upToDateWhen { false }
rootProject.apply plugin: 'jacoco'
- tasks.withType(Test) {
- jacoco.includeNoLocationClasses = true
- }
- def projects = collectProjects { ['vector','matrix-sdk-android'].contains(it.name) }
- dependsOn {
- [':vector:testGplayDebugUnitTest'] +
- [':vector:connectedGplayDebugAndroidTest'] +
- [':matrix-sdk-android:testDebugUnitTest'] +
- [':matrix-sdk-android:connectedDebugAndroidTest']
- }
+ def projects = collectProjects { ['vector', 'matrix-sdk-android'].contains(it.name) }
initializeReport(it, projects, excludes)
}
+
+task unitTestsWithCoverage(type: GradleBuild) {
+ // the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage
+ startParameter.projectProperties.coverage = [enableTestCoverage: false]
+ tasks = [':vector:testGplayDebugUnitTest', ':matrix-sdk-android:testDebugUnitTest']
+}
+
+task instrumentationTestsWithCoverage(type: GradleBuild) {
+ startParameter.projectProperties.coverage = [enableTestCoverage: true]
+ startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
+ tasks = [':vector:connectedGplayDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
+}
diff --git a/dependencies.gradle b/dependencies.gradle
index 962f07f21f..db9278b975 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -13,7 +13,7 @@ ext.versions = [
def gradle = "7.1.3"
// Ref: https://kotlinlang.org/releases.html
def kotlin = "1.6.21"
-def kotlinCoroutines = "1.6.2"
+def kotlinCoroutines = "1.6.3"
def dagger = "2.42"
def retrofit = "2.9.0"
def arrow = "0.8.2"
@@ -21,20 +21,21 @@ def markwon = "4.6.2"
def moshi = "1.13.0"
def lifecycle = "2.4.1"
def flowBinding = "1.2.0"
+def flipper = "0.151.1"
def epoxy = "4.6.2"
-def mavericks = "2.6.1"
+def mavericks = "2.7.0"
def glide = "4.13.2"
def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
def vanniktechEmoji = "0.15.0"
+def fragment = "1.4.1"
+
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
def espresso = "3.4.0"
def androidxTest = "1.4.0"
def androidxOrchestrator = "1.4.1"
-
-
ext.libs = [
gradle : [
'gradlePlugin' : "com.android.tools.build:gradle:$gradle",
@@ -48,13 +49,16 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
- 'annotation' : "androidx.annotation:annotation:1.3.0",
+ 'annotation' : "androidx.annotation:annotation:1.4.0",
'activity' : "androidx.activity:activity:1.4.0",
+ 'annotations' : "androidx.annotation:annotation:1.3.0",
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
+ 'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.8.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
- 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
+ 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
+ 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0",
@@ -85,8 +89,13 @@ ext.libs = [
'dagger' : "com.google.dagger:dagger:$dagger",
'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger",
'hilt' : "com.google.dagger:hilt-android:$dagger",
+ 'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger",
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
],
+ flipper : [
+ 'flipper' : "com.facebook.flipper:flipper:$flipper",
+ 'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper",
+ ],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
@@ -155,3 +164,5 @@ ext.libs = [
'junit' : "junit:junit:4.13.2"
]
]
+
+
diff --git a/docs/pull_request.md b/docs/pull_request.md
index d2d2bb7a3b..eebf2814a9 100644
--- a/docs/pull_request.md
+++ b/docs/pull_request.md
@@ -83,15 +83,16 @@ Exceptions can occur:
##### PR Review Assignment
-We use automatic assignment for PR reviews. A PR is automatically routed by GitHub to 2 team members using the round robin algorithm. The process is the following:
+We use automatic assignment for PR reviews. **A PR is automatically routed by GitHub to one team member** using the round robin algorithm. Additional reviewers can be used for complex changes or when the first reviewer is not confident enough on the changes.
+The process is the following:
-- The PR creator can assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at the PR.
-- If there are missing reviewers, the PR creator assigns the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer.
-- GitHub automatically assigns other reviewers. If one of the chosen reviewers is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer.
+- The PR creator selects the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer.
+- GitHub automatically assign the reviewer. If the reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer.
+- Alternatively, the PR creator can directly assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at their PR.
- Reviewers get a notification to make the review: they review the code following the good practice (see the rest of this document).
- After making their own review, if they feel not confident enough, they can ask another person for a full review, or they can tag someone within a PR comment to check specific lines.
-For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any members directly.
+For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any member directly.
##### PR review time
@@ -102,6 +103,7 @@ Some tips to achieve it:
- Set up your GH notifications correctly
- Check your pulls page: [https://github.com/pulls](https://github.com/pulls)
- Check your pending assigned PRs before starting or resuming your day to day tasks
+- If you are busy with high priority tasks, inform the author. They will find another developer
It is hard to define a deadline for a review. It depends on the PR size and the complexity. Let's start with a goal of 24h (working day!) for a PR smaller than 500 lines. If bigger, the submitter and the reviewer should discuss.
diff --git a/fastlane/metadata/android/en-US/changelogs/40104230.txt b/fastlane/metadata/android/en-US/changelogs/40104230.txt
new file mode 100644
index 0000000000..61db61727a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104230.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/en-US/changelogs/40104240.txt b/fastlane/metadata/android/en-US/changelogs/40104240.txt
new file mode 100644
index 0000000000..61db61727a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104240.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/en-US/changelogs/40104250.txt b/fastlane/metadata/android/en-US/changelogs/40104250.txt
new file mode 100644
index 0000000000..61db61727a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40104250.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/library/ui-styles/build.gradle b/library/ui-styles/build.gradle
index 31cfdd24c7..eabd0f36f6 100644
--- a/library/ui-styles/build.gradle
+++ b/library/ui-styles/build.gradle
@@ -56,8 +56,6 @@ dependencies {
implementation libs.google.material
// Pref theme
implementation libs.androidx.preferenceKtx
- // PFLockScreen attrs
- implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
// dialpad dimen
implementation 'im.dlg:android-dialer:1.2.5'
}
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_background.xml
new file mode 100644
index 0000000000..5688c433f7
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml
new file mode 100644
index 0000000000..87fa99063c
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml
new file mode 100644
index 0000000000..abde6087e0
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml
new file mode 100644
index 0000000000..e3f1082324
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml
new file mode 100644
index 0000000000..3fdebfbbe0
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml
new file mode 100644
index 0000000000..5de4957a3b
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml b/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml
new file mode 100644
index 0000000000..e1d70e8f41
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml b/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml
new file mode 100644
index 0000000000..7f0abe850a
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml
new file mode 100644
index 0000000000..b205b2d91c
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml
@@ -0,0 +1,29 @@
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml
new file mode 100644
index 0000000000..141f2ac698
--- /dev/null
+++ b/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml b/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml
new file mode 100644
index 0000000000..2ae3ca0689
--- /dev/null
+++ b/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml
@@ -0,0 +1,5 @@
+
+
+ 60dp
+ 15dp
+
diff --git a/library/ui-styles/src/main/res/values/lockscreen_attr.xml b/library/ui-styles/src/main/res/values/lockscreen_attr.xml
new file mode 100644
index 0000000000..64e77d3c4e
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/lockscreen_attr.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml b/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml
new file mode 100644
index 0000000000..eb9115d636
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml
@@ -0,0 +1,8 @@
+
+
+ #ffffff
+ #66ffffff
+ #42000000
+ #f4511e
+ #009688
+
diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml b/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml
new file mode 100644
index 0000000000..7d30f179a6
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml
@@ -0,0 +1,7 @@
+
+
+ 70dp
+ 25dp
+ 10dp
+ 5dp
+
diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml b/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml
new file mode 100644
index 0000000000..f0d7a75851
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml
@@ -0,0 +1,17 @@
+
+ Cancel
+ Use pin
+ Sign in
+ Next
+ Forgot?
+ Input pin code or use biometric authentication
+ Fingerprint not recognized. Try again
+ Fingerprint recognized
+
+ Confirm fingerprint to continue
+ Touch sensor
+ Fingerprint icon
+ Confirm PIN
+ Logo
+
+
diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml b/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml
new file mode 100644
index 0000000000..dba92df0bb
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_bottom_sheet.xml b/library/ui-styles/src/main/res/values/styles_bottom_sheet.xml
index 9f17342ede..f6c30040d9 100644
--- a/library/ui-styles/src/main/res/values/styles_bottom_sheet.xml
+++ b/library/ui-styles/src/main/res/values/styles_bottom_sheet.xml
@@ -4,10 +4,13 @@
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml
index 004aca5aaa..c8dcacb8ed 100644
--- a/library/ui-styles/src/main/res/values/styles_buttons.xml
+++ b/library/ui-styles/src/main/res/values/styles_buttons.xml
@@ -65,4 +65,4 @@
- ?colorOnPrimary
-
\ No newline at end of file
+
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 cb22863694..8459778e29 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
@@ -22,13 +22,13 @@
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index 733f7e8eb5..f86a05ed66 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -111,14 +111,14 @@
- @style/PreferenceThemeOverlay.v14.Material
- - @style/PinCodeScreenStyle
- - @style/PinCodeKeyButtonStyle
- - @style/PinCodeTitleStyle
- - @style/PinCodeHintStyle
- - @style/PinCodeDotsViewStyle
- - @style/PinCodeDeleteButtonStyle
- - @style/PinCodeFingerprintButtonStyle
- - @style/PinCodeNextButtonStyle
+ - @style/PinCodeScreenStyle
+ - @style/PinCodeKeyButtonStyle
+ - @style/PinCodeTitleStyle
+ - @style/PinCodeHintStyle
+ - @style/PinCodeDotsViewStyle
+ - @style/PinCodeDeleteButtonStyle
+ - @style/PinCodeFingerprintButtonStyle
+ - @style/PinCodeNextButtonStyle
- @color/android_status_bar_background_dark
- @color/android_navigation_bar_background_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 77996c8ce5..173b502dcd 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -111,14 +111,14 @@
- @style/PreferenceThemeOverlay.v14.Material
- - @style/PinCodeScreenStyle
- - @style/PinCodeKeyButtonStyle
- - @style/PinCodeTitleStyle
- - @style/PinCodeHintStyle
- - @style/PinCodeDotsViewStyle
- - @style/PinCodeDeleteButtonStyle
- - @style/PinCodeFingerprintButtonStyle
- - @style/PinCodeNextButtonStyle
+ - @style/PinCodeScreenStyle
+ - @style/PinCodeKeyButtonStyle
+ - @style/PinCodeTitleStyle
+ - @style/PinCodeHintStyle
+ - @style/PinCodeDotsViewStyle
+ - @style/PinCodeDeleteButtonStyle
+ - @style/PinCodeFingerprintButtonStyle
+ - @style/PinCodeNextButtonStyle
- @color/android_status_bar_background_dark
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 034b549d85..b7c4cacdde 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -5,6 +5,10 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'realm-android'
apply plugin: "org.jetbrains.dokka"
+if (project.hasProperty("coverage")) {
+ apply plugin: 'jacoco'
+}
+
buildscript {
repositories {
// Do not use `mavenCentral()`, it prevents Dependabot from working properly
@@ -56,7 +60,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- buildConfigField "String", "SDK_VERSION", "\"1.4.24\""
+ buildConfigField "String", "SDK_VERSION", "\"1.4.26\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@@ -74,7 +78,9 @@ android {
buildTypes {
debug {
- testCoverageEnabled true
+ if (project.hasProperty("coverage")) {
+ testCoverageEnabled = coverage.enableTestCoverage
+ }
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
// Set to BODY instead of NONE to enable logging
@@ -193,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.50'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51'
testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
similarity index 78%
rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
index b08c88fb24..d0d64491ef 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk
-import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
var value: Int = 0
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
similarity index 62%
rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
index 6bcd12742b..14f985243c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
@@ -14,40 +14,57 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.api.securestorage
import android.os.Build
+import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.spyk
+import org.amshove.kluent.invoking
import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.amshove.kluent.shouldNotThrow
+import org.amshove.kluent.shouldThrow
+import org.junit.Before
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.util.fromBase64
-import org.matrix.android.sdk.api.util.toBase64NoPadding
+import org.matrix.android.sdk.TestBuildVersionSdkIntProvider
import java.io.ByteArrayOutputStream
+import java.security.KeyStore
+import java.security.KeyStoreException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
-class SecretStoringUtilsTest : InstrumentedTest {
+class SecretStoringUtilsTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
- private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider)
+ private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) }
+ private val secretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
companion object {
const val TEST_STR = "This is something I want to store safely!"
}
+ @Before
+ fun setup() {
+ clearAllMocks()
+ }
+
@Test
fun testStringNominalCaseApi21() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
- val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+ val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
// Decrypt
- val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+ val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@@ -57,9 +74,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Encrypt
- val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+ val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
// Decrypt
- val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+ val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@@ -69,9 +86,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
// Encrypt
- val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+ val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
// Decrypt
- val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+ val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@@ -81,13 +98,13 @@ class SecretStoringUtilsTest : InstrumentedTest {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
- val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+ val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
// Simulate a system upgrade
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Decrypt
- val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+ val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@@ -180,5 +197,56 @@ class SecretStoringUtilsTest : InstrumentedTest {
secretStoringUtils.safeDeleteKey(alias)
}
+ @Test
+ fun testEnsureKeyReturnsSymmetricKeyOnAndroidM() {
+ buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+ val alias = generateAlias()
+
+ val key = secretStoringUtils.ensureKey(alias)
+ key shouldBeInstanceOf KeyStore.SecretKeyEntry::class
+
+ secretStoringUtils.safeDeleteKey(alias)
+ }
+
+ @Test
+ fun testEnsureKeyReturnsPrivateKeyOnAndroidL() {
+ buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+ val alias = generateAlias()
+
+ val key = secretStoringUtils.ensureKey(alias)
+ key shouldBeInstanceOf KeyStore.PrivateKeyEntry::class
+
+ secretStoringUtils.safeDeleteKey(alias)
+ }
+
+ @Test
+ fun testSafeDeleteCanHandleKeyStoreExceptions() {
+ every { keyStore.deleteEntry(any()) } throws KeyStoreException()
+
+ invoking { secretStoringUtils.safeDeleteKey(generateAlias()) } shouldNotThrow KeyStoreException::class
+ }
+
+ @Test
+ fun testLoadSecureSecretBytesWillThrowOnInvalidStreamFormat() {
+ invoking {
+ secretStoringUtils.loadSecureSecretBytes(byteArrayOf(255.toByte()), generateAlias())
+ } shouldThrow IllegalArgumentException::class
+ }
+
+ @Test
+ fun testLoadSecureSecretWillThrowOnInvalidStreamFormat() {
+ invoking {
+ secretStoringUtils.loadSecureSecret(byteArrayOf(255.toByte()).inputStream(), generateAlias())
+ } shouldThrow IllegalArgumentException::class
+ }
+
private fun generateAlias() = UUID.randomUUID().toString()
}
+
+private fun ByteArray.toBase64NoPadding(): String {
+ return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
+}
+
+private fun String.fromBase64(): ByteArray {
+ return Base64.decode(this, Base64.DEFAULT)
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
index 52beb1b484..6cf01d4ae2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
@@ -20,6 +20,7 @@ import android.content.Context
import dagger.BindsInstance
import dagger.Component
import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
import org.matrix.android.sdk.internal.auth.AuthModule
import org.matrix.android.sdk.internal.debug.DebugModule
import org.matrix.android.sdk.internal.di.MatrixComponent
@@ -39,7 +40,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
RawModule::class,
DebugModule::class,
SettingsModule::class,
- SystemModule::class
+ SystemModule::class,
+ SecureStorageModule::class,
]
)
@MatrixScope
@@ -51,7 +53,7 @@ internal interface TestMatrixComponent : MatrixComponent {
interface Factory {
fun create(
@BindsInstance context: Context,
- @BindsInstance matrixConfiguration: MatrixConfiguration
+ @BindsInstance matrixConfiguration: MatrixConfiguration,
): TestMatrixComponent
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
index 986d58741c..7b0d57abbc 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
@@ -22,7 +22,6 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeEqualTo
-import org.amshove.kluent.shouldBeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,13 +29,11 @@ import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.merge
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.util.time.DefaultClock
-import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent
@RunWith(AndroidJUnit4::class)
@@ -97,63 +94,6 @@ internal class ChunkEntityTest : InstrumentedTest {
}
}
- @Test
- fun merge_shouldAddEvents_whenMergingBackward() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
- chunk1.timelineEvents.size shouldBeEqualTo 60
- }
- }
-
- @Test
- fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- val eventsForChunk1 = createFakeListOfEvents(30)
- val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
- chunk1.isLastForward = true
- chunk2.isLastForward = false
- chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
- chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
- chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
- chunk1.timelineEvents.size shouldBeEqualTo 40
- chunk1.isLastForward.shouldBeTrue()
- }
- }
-
- @Test
- fun merge_shouldPrevTokenMerged_whenMergingForwards() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- val prevToken = "prev_token"
- chunk1.prevToken = prevToken
- chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
- chunk1.prevToken shouldBeEqualTo prevToken
- }
- }
-
- @Test
- fun merge_shouldNextTokenMerged_whenMergingBackwards() {
- monarchy.runTransactionSync { realm ->
- val chunk1: ChunkEntity = realm.createObject()
- val chunk2: ChunkEntity = realm.createObject()
- val nextToken = "next_token"
- chunk1.nextToken = nextToken
- chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
- chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
- chunk1.nextToken shouldBeEqualTo nextToken
- }
- }
-
private fun ChunkEntity.addAll(
roomId: String,
events: List,
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 3dd3f5fa2a..3dbf206e08 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
@@ -163,6 +163,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
// Ask for a forward pagination
val snapshot = runBlocking {
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
+ // We should paginate one more time to check we are at the end now that chunks are not merged.
+ aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
}
// 7 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
snapshot.size == 7 + numberOfMessagesToSend &&
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 03ab6e6767..7c1a097b24 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
@@ -20,6 +20,7 @@ import androidx.test.filters.LargeTest
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -39,6 +40,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
+@Ignore("This test will be ignored until it is fixed")
@LargeTest
class TimelinePreviousLastForwardTest : InstrumentedTest {
@@ -229,6 +231,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
bobTimeline.addListener(eventsListener)
+ bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 55569580a4..953ebddcbf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -17,6 +17,8 @@
package org.matrix.android.sdk.api
import android.content.Context
+import android.os.Handler
+import android.os.Looper
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import androidx.work.WorkManager
@@ -30,6 +32,7 @@ import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.network.ApiInterceptorListener
import org.matrix.android.sdk.api.network.ApiPath
import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
@@ -64,6 +67,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var apiInterceptor: ApiInterceptor
@Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory
@Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage
+ @Inject internal lateinit var secureStorageService: SecureStorageService
+
+ private val uiHandler = Handler(Looper.getMainLooper())
init {
val appContext = context.applicationContext
@@ -76,7 +82,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
.build()
WorkManager.initialize(appContext, configuration)
}
- ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
+ uiHandler.post {
+ ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
+ }
}
/**
@@ -115,6 +123,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
*/
fun legacySessionImporter() = legacySessionImporter
+ /**
+ * Returns the SecureStorageService used to encrypt and decrypt sensitive data.
+ */
+ fun secureStorageService(): SecureStorageService = secureStorageService
+
/**
* Get the worker factory. The returned value has to be provided to `WorkConfiguration.Builder()`.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
index 7d1407c0d8..5b6c1897bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
@@ -21,5 +21,6 @@ data class LoginFlowResult(
val ssoIdentityProviders: List?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
- val isOutdatedHomeserver: Boolean
+ val isOutdatedHomeserver: Boolean,
+ val isLogoutDevicesSupported: Boolean
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
index 5b8d2328c7..145cdbdc22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
@@ -72,7 +72,9 @@ interface LoginWizard {
* Confirm the new password, once the user has checked their email
* When this method succeed, tha account password will be effectively modified.
*
- * @param newPassword the desired new password
+ * @param newPassword the desired new password.
+ * @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
+ * if [org.matrix.android.sdk.api.auth.data.LoginFlowResult.isLogoutDevicesSupported] is true.
*/
- suspend fun resetPasswordMailConfirmed(newPassword: String)
+ suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean = true)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
similarity index 82%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
index 8b35bd173e..bd2a1078b2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
@@ -16,7 +16,7 @@
@file:Suppress("DEPRECATION")
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.api.securestorage
import android.annotation.SuppressLint
import android.content.Context
@@ -25,7 +25,7 @@ import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
-import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -80,9 +80,11 @@ import javax.security.auth.x500.X500Principal
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
* add a pin or change the schema); So you might and with a useless pile of bytes.
*/
-internal class SecretStoringUtils @Inject constructor(
+class SecretStoringUtils @Inject constructor(
private val context: Context,
- private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider
+ private val keyStore: KeyStore,
+ private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+ private val keyNeedsUserAuthentication: Boolean = false,
) {
companion object {
@@ -94,14 +96,24 @@ internal class SecretStoringUtils @Inject constructor(
private const val FORMAT_1: Byte = 1
}
- private val keyStore: KeyStore by lazy {
- KeyStore.getInstance(ANDROID_KEY_STORE).apply {
- load(null)
- }
- }
-
private val secureRandom = SecureRandom()
+ /**
+ * Allows creation of the crypto keys associated witht he [alias] before encrypting some value with it.
+ * @return A [KeyStore.Entry] with the keys.
+ */
+ @SuppressLint("NewApi")
+ fun ensureKey(alias: String): KeyStore.Entry {
+ when {
+ buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> getOrGenerateSymmetricKeyForAliasM(alias)
+ else -> getOrGenerateKeyPairForAlias(alias).privateKey
+ }
+ return keyStore.getEntry(alias, null)
+ }
+
+ /**
+ * Deletes the key associated with the [keyAlias] and logs any [KeyStoreException] that could happen.
+ */
fun safeDeleteKey(keyAlias: String) {
try {
keyStore.deleteEntry(keyAlias)
@@ -121,24 +133,24 @@ internal class SecretStoringUtils @Inject constructor(
*/
@SuppressLint("NewApi")
@Throws(Exception::class)
- fun securelyStoreString(secret: String, keyAlias: String): ByteArray {
+ fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
return when {
- buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
- else -> encryptString(secret, keyAlias)
+ buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias)
+ else -> encryptBytes(secret, keyAlias)
}
}
/**
- * Decrypt a secret that was encrypted by #securelyStoreString().
+ * Decrypt a secret that was encrypted by [securelyStoreBytes].
*/
@SuppressLint("NewApi")
@Throws(Exception::class)
- fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String {
+ fun loadSecureSecretBytes(encrypted: ByteArray, keyAlias: String): ByteArray {
encrypted.inputStream().use { inputStream ->
// First get the format
return when (val format = inputStream.read().toByte()) {
- FORMAT_API_M -> decryptStringM(inputStream, keyAlias)
- FORMAT_1 -> decryptString(inputStream, keyAlias)
+ FORMAT_API_M -> decryptBytesM(inputStream, keyAlias)
+ FORMAT_1 -> decryptBytes(inputStream, keyAlias)
else -> throw IllegalArgumentException("Unknown format $format")
}
}
@@ -162,6 +174,22 @@ internal class SecretStoringUtils @Inject constructor(
}
}
+ fun getEncryptCipher(alias: String): Cipher {
+ val key = when (val keyEntry = ensureKey(alias)) {
+ is KeyStore.SecretKeyEntry -> keyEntry.secretKey
+ is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
+ else -> throw IllegalStateException("Unknown KeyEntry type.")
+ }
+ val cipherMode = when {
+ buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
+ else -> RSA_MODE
+ }
+ val cipher = Cipher.getInstance(cipherMode)
+ cipher.init(Cipher.ENCRYPT_MODE, key)
+ return cipher
+ }
+
+ @SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.M)
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
@@ -176,6 +204,13 @@ internal class SecretStoringUtils @Inject constructor(
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(128)
+ .apply {
+ setUserAuthenticationRequired(keyNeedsUserAuthentication)
+ if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) {
+ setInvalidatedByBiometricEnrollment(true)
+ }
+ }
+ .setUserAuthenticationRequired(keyNeedsUserAuthentication)
.build()
generator.init(keyGenSpec)
return generator.generateKey()
@@ -216,19 +251,16 @@ internal class SecretStoringUtils @Inject constructor(
}
@RequiresApi(Build.VERSION_CODES.M)
- private fun encryptStringM(text: String, keyAlias: String): ByteArray {
- val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
-
- val cipher = Cipher.getInstance(AES_MODE)
- cipher.init(Cipher.ENCRYPT_MODE, secretKey)
+ private fun encryptBytesM(byteArray: ByteArray, keyAlias: String): ByteArray {
+ val cipher = getEncryptCipher(keyAlias)
val iv = cipher.iv
// we happen the iv to the final result
- val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+ val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
return formatMMake(iv, encryptedBytes)
}
@RequiresApi(Build.VERSION_CODES.M)
- private fun decryptStringM(inputStream: InputStream, keyAlias: String): String {
+ private fun decryptBytesM(inputStream: InputStream, keyAlias: String): ByteArray {
val (iv, encryptedText) = formatMExtract(inputStream)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
@@ -237,10 +269,10 @@ internal class SecretStoringUtils @Inject constructor(
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
- return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
+ return cipher.doFinal(encryptedText)
}
- private fun encryptString(text: String, keyAlias: String): ByteArray {
+ private fun encryptBytes(byteArray: ByteArray, keyAlias: String): ByteArray {
// we generate a random symmetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
@@ -252,12 +284,12 @@ internal class SecretStoringUtils @Inject constructor(
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
- val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+ val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
return format1Make(encryptedKey, iv, encryptedBytes)
}
- private fun decryptString(inputStream: InputStream, keyAlias: String): String {
+ private fun decryptBytes(inputStream: InputStream, keyAlias: String): ByteArray {
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
// we need to decrypt the key
@@ -266,16 +298,13 @@ internal class SecretStoringUtils @Inject constructor(
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
- return String(cipher.doFinal(encrypted), Charsets.UTF_8)
+ return cipher.doFinal(encrypted)
}
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
- val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
-
- val cipher = Cipher.getInstance(AES_MODE)
- cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
+ val cipher = getEncryptCipher(keyAlias)
val iv = cipher.iv
val bos1 = ByteArrayOutputStream()
@@ -362,10 +391,8 @@ internal class SecretStoringUtils @Inject constructor(
@Throws(Exception::class)
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
- val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
// Encrypt the text
- val inputCipher = Cipher.getInstance(RSA_MODE)
- inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
+ val inputCipher = getEncryptCipher(alias)
val outputStream = ByteArrayOutputStream()
CipherOutputStream(outputStream, inputCipher).use {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt
new file mode 100644
index 0000000000..37a40fd677
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.securestorage
+
+import android.content.Context
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
+import java.security.KeyStore
+
+@Module
+internal abstract class SecureStorageModule {
+
+ @Module
+ companion object {
+ @Provides
+ fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
+
+ @Provides
+ fun provideSecretStoringUtils(
+ context: Context,
+ keyStore: KeyStore,
+ buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+ ): SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
+ }
+
+ @Binds
+ abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
index 6b75c94cb2..e217611d96 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.api.session.securestorage
+package org.matrix.android.sdk.api.securestorage
import java.io.InputStream
import java.io.OutputStream
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index a0d122635d..1b01239de5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.search.SearchService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
@@ -200,11 +199,6 @@ interface Session {
*/
fun syncService(): SyncService
- /**
- * Returns the SecureStorageService associated with the session.
- */
- fun secureStorageService(): SecureStorageService
-
/**
* Returns the ProfileService associated with the session.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
index e3d52adfc5..094c66f6f7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
@@ -24,13 +24,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
interface AccountService {
/**
* Ask the homeserver to change the password.
+ *
* @param password Current password.
* @param newPassword New password
+ * @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
+ * if [org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.canControlLogoutDevices] is true.
*/
- suspend fun changePassword(
- password: String,
- newPassword: String
- )
+ suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean = true)
/**
* Deactivate the account.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index c78fb9cf79..b5d6d891e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -54,7 +54,12 @@ data class HomeServerCapabilities(
/**
* True if the home server support threading.
*/
- val canUseThreading: Boolean = false
+ val canUseThreading: Boolean = false,
+
+ /**
+ * True if the home server supports controlling the logout of all devices when changing password.
+ */
+ val canControlLogoutDevices: Boolean = false
) {
enum class RoomCapabilitySupport {
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
index dd48d51f45..ada3dc85d7 100644
--- 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
@@ -16,12 +16,58 @@
package org.matrix.android.sdk.api.session.room.location
+import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
/**
* Manage all location sharing related features.
*/
interface LocationSharingService {
+ /**
+ * Send a static location event to the room.
+ * @param latitude required latitude of the location
+ * @param longitude required longitude of the location
+ * @param uncertainty Accuracy of the location in meters
+ * @param isUserLocation indicates whether the location data corresponds to the user location or not (pinned location)
+ */
+ suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
+
+ /**
+ * Send a live location event to the room.
+ * To get the beacon info event id, [startLiveLocationShare] must be called before sending live location updates.
+ * @param beaconInfoEventId event id of the initial beacon info state event
+ * @param latitude required latitude of the location
+ * @param longitude required longitude of the location
+ * @param uncertainty Accuracy of the location in meters
+ */
+ suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
+
+ /**
+ * Starts sharing live location in the room.
+ * @param timeoutMillis timeout of the live in milliseconds
+ * @return the result of the update of the live
+ */
+ suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
+
+ /**
+ * Stops sharing live location in the room.
+ * @return the result of the update of the live
+ */
+ suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
+
+ /**
+ * Returns a LiveData on the list of current running live location shares.
+ */
+ @MainThread
fun getRunningLiveLocationShareSummaries(): LiveData>
+
+ /**
+ * Returns a LiveData on the live location share summary with the given eventId.
+ * @param beaconInfoEventId event id of the initial beacon info state event
+ */
+ @MainThread
+ fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData>
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt
new file mode 100644
index 0000000000..6f8c03be46
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+/**
+ * Represents the result of an update of live location share like a start or a stop.
+ */
+sealed interface UpdateLiveLocationShareResult {
+ data class Success(val beaconEventId: String) : UpdateLiveLocationShareResult
+ data class Failure(val error: Throwable) : UpdateLiveLocationShareResult
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
index a1fd3bd2ec..e0a7846167 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
@@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LocationInfo(
/**
- * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
+ * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
*/
@Json(name = "uri") val geoUri: String? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index 0a66a6e400..30420fd3c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -35,7 +35,7 @@ data class MessageLocationContent(
@Json(name = "body") override val body: String,
/**
- * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
+ * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
*/
@Json(name = "geo_uri") val geoUri: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
index 81b034a809..ee31d5959e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -25,4 +25,7 @@ data class PollCreationInfo(
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
@Json(name = "max_selections") val maxSelections: Int = 1,
@Json(name = "answers") val answers: List? = null
-)
+) {
+
+ fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 661c3be5bd..9cf062356f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -142,24 +142,6 @@ interface SendService {
*/
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
- /**
- * Send a location event to the room.
- * @param latitude required latitude of the location
- * @param longitude required longitude of the location
- * @param uncertainty Accuracy of the location in meters
- * @param isUserLocation indicates whether the location data corresponds to the user location or not
- */
- fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
-
- /**
- * Send a live location event to the room. beacon_info state event has to be sent before sending live location updates.
- * @param beaconInfoEventId event id of the initial beacon info state event
- * @param latitude required latitude of the location
- * @param longitude required longitude of the location
- * @param uncertainty Accuracy of the location in meters
- */
- fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
-
/**
* Remove this failed message from the timeline.
* @param localEcho the unsent local echo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index 49c0debe1b..6ca63c2c49 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -66,19 +66,6 @@ interface StateService {
*/
suspend fun deleteAvatar()
- /**
- * Stops sharing live location in the room.
- * @param userId user id
- */
- suspend fun stopLiveLocation(userId: String)
-
- /**
- * Returns beacon info state event of a user.
- * @param userId user id who is sharing location
- * @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included
- */
- suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event?
-
/**
* Send a state event to the room.
* @param eventType The type of event to send.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
similarity index 87%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
index 515656049a..b7ea187ec5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.util.system
+package org.matrix.android.sdk.api.util
-internal interface BuildVersionSdkIntProvider {
+interface BuildVersionSdkIntProvider {
/**
* Return the current version of the Android SDK.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
similarity index 85%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
index 806c6e9735..7f0024cafa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.util.system
+package org.matrix.android.sdk.api.util
import android.os.Build
import javax.inject.Inject
-internal class DefaultBuildVersionSdkIntProvider @Inject constructor() :
+class DefaultBuildVersionSdkIntProvider @Inject constructor() :
BuildVersionSdkIntProvider {
override fun get() = Build.VERSION.SDK_INT
}
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 92852e4722..9d6b018a67 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
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard
import org.matrix.android.sdk.internal.auth.login.DirectLoginTask
import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
import org.matrix.android.sdk.internal.di.Unauthenticated
@@ -292,7 +293,8 @@ internal class DefaultAuthenticationService @Inject constructor(
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl,
- isOutdatedHomeserver = !versions.isSupportedBySdk()
+ isOutdatedHomeserver = !versions.isSupportedBySdk(),
+ isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
index 20b056f1c7..656a4f671b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
@@ -121,12 +121,13 @@ internal class DefaultLoginWizard(
.also { pendingSessionStore.savePendingSessionData(it) }
}
- override suspend fun resetPasswordMailConfirmed(newPassword: String) {
+ override suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean) {
val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
- newPassword
+ newPassword,
+ logoutAllDevices
)
executeRequest(null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
index 4e0c000f87..01481f70dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
@@ -30,13 +30,17 @@ internal data class ResetPasswordMailConfirmed(
// the new password
@Json(name = "new_password")
- val newPassword: String? = null
+ val newPassword: String? = null,
+
+ @Json(name = "logout_devices")
+ val logoutDevices: Boolean? = null
) {
companion object {
- fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
+ fun create(clientSecret: String, sid: String, newPassword: String, logoutDevices: Boolean?): ResetPasswordMailConfirmed {
return ResetPasswordMailConfirmed(
auth = AuthParams.createForResetPassword(clientSecret, sid),
- newPassword = newPassword
+ newPassword = newPassword,
+ logoutDevices = logoutDevices
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index cd38b68a85..75639c6a21 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -58,6 +58,7 @@ internal data class HomeServerVersion(
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+ val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index cee4b12138..915b25134b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -111,6 +111,15 @@ private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}
+/**
+ * Indicate if the server supports MSC2457 `logout_devices` parameter when setting a new password.
+ *
+ * @return true if logout_devices is supported
+ */
+internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
+ return getMaxVersion() >= HomeServerVersion.r0_6_1
+}
+
private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 9f123f0c08..821663bcff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -62,7 +62,7 @@ internal class VerificationMessageProcessor @Inject constructor(
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
- if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
+ if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
index b3a039d119..86355ceaa8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
@@ -21,7 +21,7 @@ import androidx.core.content.edit
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.BuildConfig
-import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import timber.log.Timber
import java.security.SecureRandom
import javax.inject.Inject
@@ -40,7 +40,7 @@ import javax.inject.Inject
*/
internal class RealmKeysUtils @Inject constructor(
context: Context,
- private val secretStoringUtils: SecretStoringUtils
+ private val secretStoringUtils: SecretStoringUtils,
) {
private val rng = SecureRandom()
@@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
val key = generateKeyForRealm()
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
- val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
+ val toStore = secretStoringUtils.securelyStoreBytes(encodedKey.toByteArray(), alias)
sharedPreferences.edit {
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
}
@@ -85,7 +85,7 @@ internal class RealmKeysUtils @Inject constructor(
private fun extractKeyForDatabase(alias: String): ByteArray {
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
- val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
+ val b64 = secretStoringUtils.loadSecureSecretBytes(encryptedKey, alias)
return Base64.decode(b64, Base64.NO_PADDING)
}
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 592461f927..665567bf2a 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
@@ -47,6 +47,8 @@ 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.database.migration.MigrateSessionTo030
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber
import javax.inject.Inject
@@ -61,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000
- val schemaVersion = 29L
+ val schemaVersion = 31L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -95,5 +97,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 27) MigrateSessionTo027(realm).perform()
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
if (oldVersion < 29) MigrateSessionTo029(realm).perform()
+ if (oldVersion < 30) MigrateSessionTo030(realm).perform()
+ if (oldVersion < 31) MigrateSessionTo031(realm).perform()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index ee9e2403d6..234caec970 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.database.helper
import io.realm.Realm
-import io.realm.Sort
import io.realm.kotlin.createObject
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -34,32 +33,9 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.database.query.whereRoomId
-import org.matrix.android.sdk.internal.extensions.assertIsManaged
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import timber.log.Timber
-internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) {
- assertIsManaged()
- val localRealm = this.realm
- val eventsToMerge: List
- if (direction == PaginationDirection.FORWARDS) {
- this.nextToken = chunkToMerge.nextToken
- this.isLastForward = chunkToMerge.isLastForward
- eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
- } else {
- this.prevToken = chunkToMerge.prevToken
- this.isLastBackward = chunkToMerge.isLastBackward
- eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
- }
- chunkToMerge.stateEvents.forEach { stateEvent ->
- addStateEvent(roomId, stateEvent, direction)
- }
- eventsToMerge.forEach {
- addTimelineEventFromMerge(localRealm, it, direction)
- }
-}
-
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {
if (direction == PaginationDirection.BACKWARDS) {
Timber.v("We don't keep chunk state events when paginating backward")
@@ -144,40 +120,6 @@ internal fun computeIsUnique(
}
}
-private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) {
- val eventId = timelineEventEntity.eventId
- if (timelineEvents.find(eventId) != null) {
- return
- }
- val displayIndex = nextDisplayIndex(direction)
- val localId = TimelineEventEntity.nextId(realm)
- val copied = realm.createObject().apply {
- this.localId = localId
- this.root = timelineEventEntity.root
- this.eventId = timelineEventEntity.eventId
- this.roomId = timelineEventEntity.roomId
- this.annotations = timelineEventEntity.annotations
- this.readReceipts = timelineEventEntity.readReceipts
- this.displayIndex = displayIndex
- this.senderAvatar = timelineEventEntity.senderAvatar
- this.senderName = timelineEventEntity.senderName
- this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
- }
- handleThreadSummary(realm, eventId, copied)
- timelineEvents.add(copied)
-}
-
-/**
- * Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one.
- */
-private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
- EventEntity
- .whereRoomId(realm, newTimelineEventEntity.roomId)
- .equalTo(EventEntityFields.IS_ROOT_THREAD, true)
- .equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
- .findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
-}
-
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject(eventEntity.eventId).apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
index 79a99cdfac..0a6d4bf833 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -271,7 +271,7 @@ private fun HashMap.addSenderState(realm: Realm, roo
* Create an EventEntity for the root thread event or get an existing one.
*/
private fun createEventEntity(realm: Realm, roomId: String, event: Event, currentTimeMillis: Long): EventEntity {
- val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it }
+ val ageLocalTs = currentTimeMillis - (event.unsignedData?.age ?: 0)
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index 5b60c53642..0f0a847c78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -130,7 +130,7 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
internal fun Event.toEntity(
roomId: String,
sendState: SendState,
- ageLocalTs: Long?,
+ ageLocalTs: Long,
contentToInject: String? = null
): EventEntity {
return EventMapper.map(this, roomId).apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 20af43530c..184a0108b9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -42,7 +42,8 @@ internal object HomeServerCapabilitiesMapper {
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson),
- canUseThreading = entity.canUseThreading
+ canUseThreading = entity.canUseThreading,
+ canControlLogoutDevices = entity.canControlLogoutDevices
)
}
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 9460e4c6ba..4a4c730a0b 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
@@ -16,15 +16,17 @@
package org.matrix.android.sdk.internal.database.mapper
+import com.zhuinden.monarchy.Monarchy
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 class LiveLocationShareAggregatedSummaryMapper @Inject constructor() {
+internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() :
+ Monarchy.Mapper {
- fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
+ override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
return LiveLocationShareAggregatedSummary(
userId = entity.userId,
isActive = entity.isActive,
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
index aebca11c2b..17dc0f7c82 100644
--- 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
@@ -25,7 +25,7 @@ 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) {
+internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 29) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt
new file mode 100644
index 0000000000..5d24b1433c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt
@@ -0,0 +1,61 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+/**
+ * Migrating to:
+ * Cleaning old chunks which may have broken links.
+ */
+internal class MigrateSessionTo030(realm: DynamicRealm) : RealmMigrator(realm, 30) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ // Delete all previous chunks
+ val chunks = realm.where("ChunkEntity")
+ .equalTo(ChunkEntityFields.IS_LAST_FORWARD, false)
+ .findAll()
+
+ val nbOfDeletedChunks = chunks.size
+ var nbOfDeletedTimelineEvents = 0
+ var nbOfDeletedEvents = 0
+ chunks.forEach { chunk ->
+ val timelineEvents = chunk.getList(ChunkEntityFields.TIMELINE_EVENTS.`$`)
+ timelineEvents.forEach { timelineEvent ->
+ // Don't delete state events
+ val event = timelineEvent.getObject(TimelineEventEntityFields.ROOT.`$`)
+ if (event?.isNull(EventEntityFields.STATE_KEY) == true) {
+ nbOfDeletedEvents++
+ event.deleteFromRealm()
+ }
+ }
+ nbOfDeletedTimelineEvents += timelineEvents.size
+ timelineEvents.deleteAllFromRealm()
+ }
+ chunks.deleteAllFromRealm()
+ Timber.d(
+ "MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s)," +
+ " $nbOfDeletedTimelineEvents deleted TimelineEvent(s)" +
+ " and $nbOfDeletedEvents deleted Event(s)."
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt
new file mode 100644
index 0000000000..e278b74756
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.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.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo031(realm: DynamicRealm) : RealmMigrator(realm, 31) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("HomeServerCapabilitiesEntity")
+ ?.addField(HomeServerCapabilitiesEntityFields.CAN_CONTROL_LOGOUT_DEVICES, Boolean::class.java)
+ ?.forceRefreshOfHomeServerCapabilities()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 47a83f0ed9..9d90973f8a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -29,7 +29,8 @@ internal open class HomeServerCapabilitiesEntity(
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L,
- var canUseThreading: Boolean = false
+ var canUseThreading: Boolean = false,
+ var canControlLogoutDevices: Boolean = false
) : RealmObject() {
companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
index 9350102137..1e5d96b496 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
@@ -31,6 +31,7 @@ internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQue
internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity? {
val query = where(realm, roomId)
+ if (prevToken == null && nextToken == null) return null
if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
}
@@ -40,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
return query.findFirst()
}
-internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults? {
+internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults {
val query = where(realm, roomId)
if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
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 7dfeb6884a..d69f251f6f 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
@@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
realm: Realm,
roomId: String,
userId: String,
- ignoredEventId: String
+ ignoredEventId: String,
): List {
return LiveLocationShareAggregatedSummaryEntity
.whereRoomId(realm, roomId = roomId)
@@ -84,6 +84,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
.findAll()
+ .toList()
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index d668c0498f..44ec90ed40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule
@@ -53,7 +55,8 @@ import java.io.File
DebugModule::class,
SettingsModule::class,
SystemModule::class,
- NoOpTestModule::class
+ NoOpTestModule::class,
+ SecureStorageModule::class,
]
)
@MatrixScope
@@ -96,6 +99,8 @@ internal interface MatrixComponent {
fun sessionManager(): SessionManager
+ fun secureStorageService(): SecureStorageService
+
fun matrixWorkerFactory(): MatrixWorkerFactory
fun inject(matrix: Matrix)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
index ef8133dd15..8f6605d657 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.internal.securestorage
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index 36d3f0606b..7c50a0ff84 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -55,7 +55,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.search.SearchService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.session.space.SpaceService
@@ -111,7 +110,6 @@ internal class DefaultSession @Inject constructor(
private val cryptoService: Lazy,
private val defaultFileService: Lazy,
private val permalinkService: Lazy,
- private val secureStorageService: Lazy,
private val profileService: Lazy,
private val syncService: Lazy,
private val mediaService: Lazy,
@@ -220,7 +218,6 @@ internal class DefaultSession @Inject constructor(
override fun eventService(): EventService = eventService.get()
override fun termsService(): TermsService = termsService.get()
override fun syncService(): SyncService = syncService.get()
- override fun secureStorageService(): SecureStorageService = secureStorageService.get()
override fun profileService(): ProfileService = profileService.get()
override fun presenceService(): PresenceService = presenceService.get()
override fun accountService(): AccountService = accountService.get()
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 f01451b688..d3cae3ac2d 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
@@ -20,6 +20,7 @@ import dagger.BindsInstance
import dagger.Component
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.CryptoModule
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
@@ -98,7 +99,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
ThirdPartyModule::class,
SpaceModule::class,
PresenceModule::class,
- RequestModule::class
+ RequestModule::class,
+ SecureStorageModule::class,
]
)
@SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
index 756b9cef83..0dae24e64b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
@@ -19,12 +19,13 @@ package org.matrix.android.sdk.internal.session
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
+import java.util.concurrent.CopyOnWriteArraySet
import javax.inject.Inject
@SessionScope
internal class SessionListeners @Inject constructor() {
- private val listeners = mutableSetOf()
+ private val listeners = CopyOnWriteArraySet()
fun addListener(listener: Session.Listener) {
synchronized(listeners) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 950cb899f8..f8a52f0b7e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.openid.OpenIdService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.api.util.md5
@@ -93,7 +92,6 @@ import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcesso
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor
-import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService
import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter
@@ -367,9 +365,6 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver
- @Binds
- abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
-
@Binds
abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
index 1b95820918..f6778327d9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
@@ -29,13 +29,17 @@ internal data class ChangePasswordParams(
val auth: UserPasswordAuth? = null,
@Json(name = "new_password")
- val newPassword: String? = null
+ val newPassword: String? = null,
+
+ @Json(name = "logout_devices")
+ val logoutDevices: Boolean = true
) {
companion object {
- fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
+ fun create(userId: String, oldPassword: String, newPassword: String, logoutAllDevices: Boolean): ChangePasswordParams {
return ChangePasswordParams(
auth = UserPasswordAuth(user = userId, password = oldPassword),
- newPassword = newPassword
+ newPassword = newPassword,
+ logoutDevices = logoutAllDevices
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
index 7b21ba2e63..e767950ff7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
@@ -26,7 +26,8 @@ import javax.inject.Inject
internal interface ChangePasswordTask : Task {
data class Params(
val password: String,
- val newPassword: String
+ val newPassword: String,
+ val logoutAllDevices: Boolean
)
}
@@ -37,7 +38,7 @@ internal class DefaultChangePasswordTask @Inject constructor(
) : ChangePasswordTask {
override suspend fun execute(params: ChangePasswordTask.Params) {
- val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword)
+ val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword, params.logoutAllDevices)
try {
executeRequest(globalErrorReceiver) {
accountAPI.changePassword(changePasswordParams)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
index bb830a5e41..9d03ec479b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
@@ -25,8 +25,8 @@ internal class DefaultAccountService @Inject constructor(
private val deactivateAccountTask: DeactivateAccountTask
) : AccountService {
- override suspend fun changePassword(password: String, newPassword: String) {
- changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
+ override suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean) {
+ changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword, logoutAllDevices))
}
override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index d22da8f6f2..add69dd8c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
@@ -142,6 +143,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getVersionResult != null) {
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
+ homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
}
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index 556e31356e..e1dd22a211 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -55,6 +55,18 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
+import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
+import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
+import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask
@@ -309,4 +321,22 @@ internal abstract class RoomModule {
@Binds
abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
+
+ @Binds
+ abstract fun bindStartLiveLocationShareTask(task: DefaultStartLiveLocationShareTask): StartLiveLocationShareTask
+
+ @Binds
+ abstract fun bindStopLiveLocationShareTask(task: DefaultStopLiveLocationShareTask): StopLiveLocationShareTask
+
+ @Binds
+ abstract fun bindSendStaticLocationTask(task: DefaultSendStaticLocationTask): SendStaticLocationTask
+
+ @Binds
+ abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask
+
+ @Binds
+ abstract fun bindGetActiveBeaconInfoForUserTask(task: DefaultGetActiveBeaconInfoForUserTask): GetActiveBeaconInfoForUserTask
+
+ @Binds
+ abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
}
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 05bde8f83f..921749122b 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
@@ -36,16 +36,22 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
-// TODO add unit tests
+/**
+ * Aggregates all live location sharing related events in local database.
+ */
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) {
+ /**
+ * Handle the content of a beacon info.
+ * @return true if it has been processed, false if ignored.
+ */
+ fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean): Boolean {
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
- return
+ return false
}
val isLive = content.isLive.orTrue()
@@ -58,7 +64,7 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
if (targetEventId.isNullOrEmpty()) {
Timber.w("no target event id found for the beacon content")
- return
+ return false
}
val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
@@ -83,6 +89,8 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
} else {
cancelDeactivationAfterTimeout(targetEventId, roomId)
}
+
+ return true
}
private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) {
@@ -110,6 +118,10 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
workManagerProvider.workManager.cancelUniqueWork(workName)
}
+ /**
+ * Handle the content of a beacon location data.
+ * @return true if it has been processed, false if ignored.
+ */
fun handleBeaconLocationData(
realm: Realm,
event: Event,
@@ -117,14 +129,14 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
roomId: String,
relatedEventId: String?,
isLocalEcho: Boolean
- ) {
+ ): Boolean {
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
- return
+ return false
}
if (relatedEventId.isNullOrEmpty()) {
Timber.w("no related event id found for the live location content")
- return
+ return false
}
val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
@@ -139,9 +151,12 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
?.getBestTimestampMillis()
?: 0
- if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
+ return if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
Timber.d("updating last location of the summary of id=$relatedEventId")
aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent())
+ true
+ } else {
+ false
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt
new file mode 100644
index 0000000000..228a046f53
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface CheckIfExistingActiveLiveTask : Task {
+ data class Params(
+ val roomId: String,
+ )
+}
+
+internal class DefaultCheckIfExistingActiveLiveTask @Inject constructor(
+ private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
+) : CheckIfExistingActiveLiveTask {
+
+ override suspend fun execute(params: CheckIfExistingActiveLiveTask.Params): Boolean {
+ val getActiveBeaconTaskParams = GetActiveBeaconInfoForUserTask.Params(
+ roomId = params.roomId
+ )
+ return getActiveBeaconInfoForUserTask.execute(getActiveBeaconTaskParams)
+ ?.getClearContent()
+ ?.toModel()
+ ?.isLive
+ .orFalse()
+ }
+}
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
index 8cf6fcdfbf..20320cad23 100644
--- 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
@@ -17,21 +17,31 @@
package org.matrix.android.sdk.internal.session.room.location
import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
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.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
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.database.query.where
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 sendStaticLocationTask: SendStaticLocationTask,
+ private val sendLiveLocationTask: SendLiveLocationTask,
+ private val startLiveLocationShareTask: StartLiveLocationShareTask,
+ private val stopLiveLocationShareTask: StopLiveLocationShareTask,
+ private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
) : LocationSharingService {
@@ -40,10 +50,72 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
fun create(roomId: String): DefaultLocationSharingService
}
+ override suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+ val params = SendStaticLocationTask.Params(
+ roomId = roomId,
+ latitude = latitude,
+ longitude = longitude,
+ uncertainty = uncertainty,
+ isUserLocation = isUserLocation,
+ )
+ return sendStaticLocationTask.execute(params)
+ }
+
+ override suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
+ val params = SendLiveLocationTask.Params(
+ beaconInfoEventId = beaconInfoEventId,
+ roomId = roomId,
+ latitude = latitude,
+ longitude = longitude,
+ uncertainty = uncertainty,
+ )
+ return sendLiveLocationTask.execute(params)
+ }
+
+ override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
+ // Ensure to stop any active live before starting a new one
+ if (checkIfExistingActiveLive()) {
+ val result = stopLiveLocationShare()
+ if (result is UpdateLiveLocationShareResult.Failure) {
+ return result
+ }
+ }
+ val params = StartLiveLocationShareTask.Params(
+ roomId = roomId,
+ timeoutMillis = timeoutMillis
+ )
+ return startLiveLocationShareTask.execute(params)
+ }
+
+ private suspend fun checkIfExistingActiveLive(): Boolean {
+ val params = CheckIfExistingActiveLiveTask.Params(
+ roomId = roomId
+ )
+ return checkIfExistingActiveLiveTask.execute(params)
+ }
+
+ override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult {
+ val params = StopLiveLocationShareTask.Params(
+ roomId = roomId,
+ )
+ return stopLiveLocationShareTask.execute(params)
+ }
+
override fun getRunningLiveLocationShareSummaries(): LiveData> {
return monarchy.findAllMappedWithChanges(
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
- { liveLocationShareAggregatedSummaryMapper.map(it) }
+ liveLocationShareAggregatedSummaryMapper
)
}
+
+ override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> {
+ return Transformations.map(
+ monarchy.findAllMappedWithChanges(
+ { LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) },
+ liveLocationShareAggregatedSummaryMapper
+ )
+ ) {
+ it.firstOrNull().toOptional()
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
new file mode 100644
index 0000000000..a8d955af1d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 org.matrix.android.sdk.api.extensions.orFalse
+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.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetActiveBeaconInfoForUserTask : Task {
+ data class Params(
+ val roomId: String,
+ )
+}
+
+internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor(
+ @UserId private val userId: String,
+ private val stateEventDataSource: StateEventDataSource,
+) : GetActiveBeaconInfoForUserTask {
+
+ override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? {
+ return EventType.STATE_ROOM_BEACON_INFO
+ .mapNotNull {
+ stateEventDataSource.getStateEvent(
+ roomId = params.roomId,
+ eventType = it,
+ stateKey = QueryStringValue.Equals(userId)
+ )
+ }
+ .firstOrNull { beaconInfoEvent ->
+ beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse()
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt
new file mode 100644
index 0000000000..bebd9c774a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.location
+
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface SendLiveLocationTask : Task {
+ data class Params(
+ val roomId: String,
+ val beaconInfoEventId: String,
+ val latitude: Double,
+ val longitude: Double,
+ val uncertainty: Double?,
+ )
+}
+
+internal class DefaultSendLiveLocationTask @Inject constructor(
+ private val localEchoEventFactory: LocalEchoEventFactory,
+ private val eventSenderProcessor: EventSenderProcessor,
+) : SendLiveLocationTask {
+
+ override suspend fun execute(params: SendLiveLocationTask.Params): Cancelable {
+ val event = localEchoEventFactory.createLiveLocationEvent(
+ beaconInfoEventId = params.beaconInfoEventId,
+ roomId = params.roomId,
+ latitude = params.latitude,
+ longitude = params.longitude,
+ uncertainty = params.uncertainty,
+ )
+ localEchoEventFactory.createLocalEcho(event)
+ return eventSenderProcessor.postEvent(event)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt
new file mode 100644
index 0000000000..e08b82f3d4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.location
+
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface SendStaticLocationTask : Task {
+ data class Params(
+ val roomId: String,
+ val latitude: Double,
+ val longitude: Double,
+ val uncertainty: Double?,
+ val isUserLocation: Boolean
+ )
+}
+
+internal class DefaultSendStaticLocationTask @Inject constructor(
+ private val localEchoEventFactory: LocalEchoEventFactory,
+ private val eventSenderProcessor: EventSenderProcessor,
+) : SendStaticLocationTask {
+
+ override suspend fun execute(params: SendStaticLocationTask.Params): Cancelable {
+ val event = localEchoEventFactory.createStaticLocationEvent(
+ roomId = params.roomId,
+ latitude = params.latitude,
+ longitude = params.longitude,
+ uncertainty = params.uncertainty,
+ isUserLocation = params.isUserLocation
+ )
+ localEchoEventFactory.createLocalEcho(event)
+ return eventSenderProcessor.postEvent(event)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
new file mode 100644
index 0000000000..b943c27977
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.time.Clock
+import javax.inject.Inject
+
+internal interface StartLiveLocationShareTask : Task {
+ data class Params(
+ val roomId: String,
+ val timeoutMillis: Long,
+ )
+}
+
+internal class DefaultStartLiveLocationShareTask @Inject constructor(
+ @UserId private val userId: String,
+ private val clock: Clock,
+ private val sendStateTask: SendStateTask,
+) : StartLiveLocationShareTask {
+
+ override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
+ val beaconContent = MessageBeaconInfoContent(
+ timeout = params.timeoutMillis,
+ isLive = true,
+ unstableTimestampMillis = clock.epochMillis()
+ ).toContent()
+ val eventType = EventType.STATE_ROOM_BEACON_INFO.first()
+ val sendStateTaskParams = SendStateTask.Params(
+ roomId = params.roomId,
+ stateKey = userId,
+ eventType = eventType,
+ body = beaconContent
+ )
+ return try {
+ val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
+ if (eventId.isNotEmpty()) {
+ UpdateLiveLocationShareResult.Success(eventId)
+ } else {
+ UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
+ }
+ } catch (error: Throwable) {
+ UpdateLiveLocationShareResult.Failure(error)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
new file mode 100644
index 0000000000..da5fd76940
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 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.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface StopLiveLocationShareTask : Task {
+ data class Params(
+ val roomId: String,
+ )
+}
+
+internal class DefaultStopLiveLocationShareTask @Inject constructor(
+ private val sendStateTask: SendStateTask,
+ private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
+) : StopLiveLocationShareTask {
+
+ override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
+ val beaconInfoStateEvent = getActiveLiveLocationBeaconInfoForUser(params.roomId) ?: return getResultForIncorrectBeaconInfoEvent()
+ val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent()
+ val content = beaconInfoStateEvent.getClearContent()?.toModel() ?: return getResultForIncorrectBeaconInfoEvent()
+ val updatedContent = content.copy(isLive = false).toContent()
+ val sendStateTaskParams = SendStateTask.Params(
+ roomId = params.roomId,
+ stateKey = stateKey,
+ eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+ body = updatedContent
+ )
+ return try {
+ val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
+ if (eventId.isNotEmpty()) {
+ UpdateLiveLocationShareResult.Success(eventId)
+ } else {
+ UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
+ }
+ } catch (error: Throwable) {
+ UpdateLiveLocationShareResult.Failure(error)
+ }
+ }
+
+ private fun getResultForIncorrectBeaconInfoEvent() =
+ UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event"))
+
+ private suspend fun getActiveLiveLocationBeaconInfoForUser(roomId: String): Event? {
+ val params = GetActiveBeaconInfoForUserTask.Params(
+ roomId = roomId
+ )
+ return getActiveBeaconInfoForUserTask.execute(params)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
index 005d7f26db..ef89ca33a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
@@ -58,7 +58,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
}
override suspend fun loadRoomMembersIfNeeded() {
- val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
+ val params = LoadRoomMembersTask.Params(roomId, excludeMembership = Membership.LEAVE)
loadRoomMembersTask.execute(params)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
index 15d0889255..7052eb23e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
+import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -105,32 +106,37 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
}
private suspend fun insertInDb(response: RoomMembersResponse, roomId: String) {
+ val chunks = response.roomMemberEvents.chunked(500)
+ chunks.forEach { roomMemberEvents ->
+ monarchy.awaitTransaction { realm ->
+ Timber.v("Insert ${roomMemberEvents.size} member events in room $roomId")
+ // We ignore all the already known members
+ val now = clock.epochMillis()
+ for (roomMemberEvent in roomMemberEvents) {
+ if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
+ continue
+ }
+ val ageLocalTs = now - (roomMemberEvent.unsignedData?.age ?: 0)
+ val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+ CurrentStateEventEntity.getOrCreate(
+ realm,
+ roomId,
+ roomMemberEvent.stateKey,
+ roomMemberEvent.type
+ ).apply {
+ eventId = roomMemberEvent.eventId
+ root = eventEntity
+ }
+ roomMemberEventHandler.handle(realm, roomId, roomMemberEvent, false)
+ }
+ }
+ }
monarchy.awaitTransaction { realm ->
- // We ignore all the already known members
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
- val now = clock.epochMillis()
- for (roomMemberEvent in response.roomMemberEvents) {
- if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
- continue
- }
- val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it }
- val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
- CurrentStateEventEntity.getOrCreate(
- realm,
- roomId,
- roomMemberEvent.stateKey,
- roomMemberEvent.type
- ).apply {
- eventId = roomMemberEvent.eventId
- root = eventEntity
- }
- roomMemberEventHandler.handle(realm, roomId, roomMemberEvent, false)
- }
roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
roomSummaryUpdater.update(realm, roomId, updateMembers = true)
}
-
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
deviceListManager.onRoomMembersLoadedFor(roomId)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
index 1e36e9c6da..fd6552525e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
@@ -83,23 +83,21 @@ internal class RoomMemberEventHandler @Inject constructor(
userId: String,
roomMember: RoomMemberContent
) {
- val roomMemberEntity = RoomMemberEntityFactory.create(
- roomId,
- userId,
- roomMember,
- // When an update is happening, insertOrUpdate replace existing values with null if they are not provided,
- // but we want to preserve presence record value and not replace it with null
- getExistingPresenceState(realm, roomId, userId)
- )
- realm.insertOrUpdate(roomMemberEntity)
- }
-
- /**
- * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
- * by NULL value.
- */
- private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
- return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
+ val existingRoomMemberSummary = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()
+ if (existingRoomMemberSummary != null) {
+ existingRoomMemberSummary.displayName = roomMember.displayName
+ existingRoomMemberSummary.avatarUrl = roomMember.avatarUrl
+ existingRoomMemberSummary.membership = roomMember.membership
+ } else {
+ val presenceEntity = UserPresenceEntity.where(realm, userId).findFirst()
+ val roomMemberEntity = RoomMemberEntityFactory.create(
+ roomId,
+ userId,
+ roomMember,
+ presenceEntity
+ )
+ realm.insert(roomMemberEntity)
+ }
}
private fun saveUserEntityLocallyIfNecessary(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index bad734173e..bac810f424 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -209,7 +209,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
* Create an EventEntity to be added in the TimelineEventEntity.
*/
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
- val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
+ val now = clock.epochMillis()
+ val ageLocalTs = now - (event.unsignedData?.age ?: 0)
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index fc78abcfd9..418000abed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -129,18 +129,6 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
- return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
- .also { createLocalEcho(it) }
- .let { sendEvent(it) }
- }
-
- override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
- return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty)
- .also { createLocalEcho(it) }
- .let { sendEvent(it) }
- }
-
override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements?
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 3b9ca44d18..f52500de1b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor(
)
}
- fun createLocationEvent(
+ fun createStaticLocationEvent(
roomId: String,
latitude: Double,
longitude: Double,
@@ -708,7 +708,7 @@ internal class LocalEchoEventFactory @Inject constructor(
}
/**
- * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30'
+ * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30'
* Uncertainty of the location is in meters and not required.
*/
private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String {
@@ -718,7 +718,7 @@ internal class LocalEchoEventFactory @Inject constructor(
append(",")
append(longitude)
uncertainty?.let {
- append(";")
+ append(";u=")
append(it)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index c15bcb1c1a..ad47b82428 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -21,33 +21,27 @@ import androidx.lifecycle.LiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStateEventValue
-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.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
-import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader
-import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
internal class DefaultStateService @AssistedInject constructor(
@Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource,
private val sendStateTask: SendStateTask,
private val fileUploader: FileUploader,
- private val viaParameterFinder: ViaParameterFinder
) : StateService {
@AssistedFactory
@@ -191,35 +185,4 @@ internal class DefaultStateService @AssistedInject constructor(
}
updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
}
-
- override suspend fun stopLiveLocation(userId: String) {
- getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent ->
- beaconInfoStateEvent.getClearContent()?.toModel()?.let { content ->
- val updatedContent = content.copy(isLive = false).toContent()
-
- beaconInfoStateEvent.stateKey?.let {
- sendStateEvent(
- eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
- body = updatedContent,
- stateKey = it
- )
- }
- }
- }
- }
-
- override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? {
- return EventType.STATE_ROOM_BEACON_INFO
- .mapNotNull {
- stateEventDataSource.getStateEvent(
- roomId = roomId,
- eventType = it,
- stateKey = QueryStringValue.Equals(userId)
- )
- }
- .firstOrNull { beaconInfoEvent ->
- !filterOnlyLive ||
- beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse()
- }
- }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 05379a1a7b..6e72cbdb8f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.Realm
import io.realm.RealmConfiguration
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.android.asCoroutineDispatcher
@@ -32,6 +33,7 @@ import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -236,11 +238,15 @@ internal class DefaultTimeline(
val loadMoreResult = try {
strategy.loadMore(count, direction, fetchOnServerIfNeeded)
} catch (throwable: Throwable) {
- // Timeline could not be loaded with a (likely) permanent issue, such as the
- // server now knowing the initialEventId, so we want to show an error message
- // and possibly restart without initialEventId.
- onTimelineFailure(throwable)
- return false
+ if (throwable is CancellationException) {
+ LoadMoreResult.FAILURE
+ } else {
+ // Timeline could not be loaded with a (likely) permanent issue, such as the
+ // server now knowing the initialEventId, so we want to show an error message
+ // and possibly restart without initialEventId.
+ onTimelineFailure(throwable)
+ return false
+ }
}
Timber.v("$baseLogMessage: result $loadMoreResult")
val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
@@ -385,7 +391,7 @@ internal class DefaultTimeline(
private suspend fun loadRoomMembersIfNeeded() {
if (RoomLocalEcho.isLocalEchoId(roomId)) return
- val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId)
+ val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId, excludeMembership = Membership.LEAVE)
try {
loadRoomMembersTask.execute(loadRoomMembersParam)
} catch (failure: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
index aef9e24c8b..7c662444e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
@@ -61,7 +61,7 @@ internal class DefaultGetEventTask @Inject constructor(
}
}
- event.ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
+ event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)
return event
}
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 fd76d5ae28..e13f3f454f 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
@@ -70,6 +70,7 @@ internal class TimelineChunk(
private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward)
+ private val nextToken = chunkEntity.nextToken
private var prevChunkLatch: CompletableDeferred? = null
private var nextChunkLatch: CompletableDeferred? = null
@@ -136,8 +137,10 @@ internal class TimelineChunk(
val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
deepBuiltItems.addAll(prevEvents)
}
-
- return deepBuiltItems
+ // In some scenario (permalink) we might end up with duplicate timeline events, so we want to be sure we only expose one.
+ return deepBuiltItems.distinctBy {
+ it.eventId
+ }
}
/**
@@ -154,10 +157,6 @@ internal class TimelineChunk(
val loadFromStorage = loadFromStorage(count, direction).also {
logLoadedFromStorage(it, direction)
}
- if (loadFromStorage.numberOfEvents == 6) {
- Timber.i("here")
- }
-
val offsetCount = count - loadFromStorage.numberOfEvents
return if (offsetCount == 0) {
@@ -251,10 +250,6 @@ internal class TimelineChunk(
}
fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
- val builtEventIndex = builtEventsIndexes[eventId]
- if (builtEventIndex != null) {
- return getOffsetIndex() + builtEventIndex
- }
if (searchInNext) {
val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false)
if (nextBuiltEventIndex != null) {
@@ -267,7 +262,12 @@ internal class TimelineChunk(
return prevBuiltEventIndex
}
}
- return null
+ val builtEventIndex = builtEventsIndexes[eventId]
+ return if (builtEventIndex != null) {
+ getOffsetIndex() + builtEventIndex
+ } else {
+ null
+ }
}
fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? {
@@ -445,7 +445,7 @@ internal class TimelineChunk(
Timber.e(failure, "Failed to fetch from server")
LoadMoreResult.FAILURE
}
- return if (loadMoreResult == LoadMoreResult.SUCCESS) {
+ return if (loadMoreResult != LoadMoreResult.FAILURE) {
latch?.await()
loadMore(count, direction, fetchOnServerIfNeeded = false)
} else {
@@ -470,11 +470,15 @@ internal class TimelineChunk(
}
private fun getOffsetIndex(): Int {
+ if (nextToken == null) return 0
var offset = 0
var currentNextChunk = nextChunk
while (currentNextChunk != null) {
offset += currentNextChunk.builtEvents.size
- currentNextChunk = currentNextChunk.nextChunk
+ currentNextChunk = currentNextChunk.nextChunk?.takeIf {
+ // In case of permalink we can end up with a linked nextChunk (which is the lastForward Chunk) but no nextToken
+ it.nextToken != null
+ }
}
return offset
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt
index b9aca7d37b..b1b9e4bb22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt
@@ -56,7 +56,8 @@ internal class TimelineEventDataSource @Inject constructor(
// TODO pretty bad query.. maybe we should denormalize clear type in base?
return realmSessionProvider.withRealm { realm ->
TimelineEventEntity.whereRoomId(realm, roomId)
- .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+ .sort(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, Sort.ASCENDING)
+ .distinct(TimelineEventEntityFields.EVENT_ID)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
.orEmpty()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt
index 465b0faac8..e9626a2173 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt
@@ -24,5 +24,5 @@ internal interface TokenChunkEvent {
val events: List
val stateEvents: List?
- fun hasMore() = start != end
+ fun hasMore() = end != null && start != end
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index da73727065..ea22f8cd78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -33,12 +33,10 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find
-import org.matrix.android.sdk.internal.database.query.findAll
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
@@ -83,27 +81,22 @@ internal class TokenChunkEventPersistor @Inject constructor(
nextToken = receivedChunk.start
prevToken = receivedChunk.end
}
-
val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken)
if (existingChunk != null) {
- Timber.v("This chunk is already in the db, checking if this might be caused by broken links")
- existingChunk.fixChunkLinks(realm, roomId, direction, prevToken, nextToken)
+ Timber.v("This chunk is already in the db, return.")
return@awaitTransaction
}
+
+ // Creates links in both directions
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply {
this.nextChunk = nextChunk
this.prevChunk = prevChunk
}
- val allNextChunks = ChunkEntity.findAll(realm, roomId, prevToken = nextToken)
- val allPrevChunks = ChunkEntity.findAll(realm, roomId, nextToken = prevToken)
- allNextChunks?.forEach {
- it.prevChunk = currentChunk
- }
- allPrevChunks?.forEach {
- it.nextChunk = currentChunk
- }
+ nextChunk?.prevChunk = currentChunk
+ prevChunk?.nextChunk = currentChunk
+
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(roomId, direction, currentChunk)
} else {
@@ -122,38 +115,13 @@ internal class TokenChunkEventPersistor @Inject constructor(
}
}
- private fun ChunkEntity.fixChunkLinks(
- realm: Realm,
- roomId: String,
- direction: PaginationDirection,
- prevToken: String?,
- nextToken: String?,
- ) {
- if (direction == PaginationDirection.FORWARDS) {
- val prevChunks = ChunkEntity.findAll(realm, roomId, nextToken = prevToken)
- Timber.v("Found ${prevChunks?.size} prevChunks")
- prevChunks?.forEach {
- if (it.nextChunk != this) {
- Timber.i("Set nextChunk for ${it.identifier()} from ${it.nextChunk?.identifier()} to ${identifier()}")
- it.nextChunk = this
- }
- }
- } else {
- val nextChunks = ChunkEntity.findAll(realm, roomId, prevToken = nextToken)
- Timber.v("Found ${nextChunks?.size} nextChunks")
- nextChunks?.forEach {
- if (it.prevChunk != this) {
- Timber.i("Set prevChunk for ${it.identifier()} from ${it.prevChunk?.identifier()} to ${identifier()}")
- it.prevChunk = this
- }
- }
- }
- }
-
private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
- Timber.v("Reach end of $roomId")
+ Timber.v("Reach end of $roomId in $direction")
if (direction == PaginationDirection.FORWARDS) {
- Timber.v("We should keep the lastForward chunk unique, the one from sync")
+ // We should keep the lastForward chunk unique, the one from sync, so make an unidirectional link.
+ // This will allow us to get live events from sync even from a permalink but won't make the link in the opposite.
+ val realm = currentChunk.realm
+ currentChunk.nextChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
} else {
currentChunk.isLastBackward = true
}
@@ -174,7 +142,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
val now = clock.epochMillis()
stateEvents?.forEach { stateEvent ->
- val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
+ val ageLocalTs = now - (stateEvent.unsignedData?.age ?: 0)
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
currentChunk.addStateEvent(roomId, stateEventEntity, direction)
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) {
@@ -187,38 +155,8 @@ internal class TokenChunkEventPersistor @Inject constructor(
if (event.eventId == null || event.senderId == null) {
return@forEach
}
- // We check for the timeline event with this id, but not in the thread chunk
- val eventId = event.eventId
- val existingTimelineEvent = TimelineEventEntity
- .where(realm, roomId, eventId)
- .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
- .findFirst()
- // If it exists, we want to stop here, just link the prevChunk
- val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
- if (existingChunk != null) {
- when (direction) {
- PaginationDirection.BACKWARDS -> {
- if (currentChunk.nextChunk == existingChunk) {
- Timber.w("Avoid double link, shouldn't happen in an ideal world")
- } else {
- currentChunk.prevChunk = existingChunk
- existingChunk.nextChunk = currentChunk
- }
- }
- PaginationDirection.FORWARDS -> {
- if (currentChunk.prevChunk == existingChunk) {
- Timber.w("Avoid double link, shouldn't happen in an ideal world")
- } else {
- currentChunk.nextChunk = existingChunk
- existingChunk.prevChunk = currentChunk
- }
- }
- }
- // Stop processing here
- return@processTimelineEvents
- }
- val ageLocalTs = event.unsignedData?.age?.let { now - it }
- var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+ val ageLocalTs = now - (event.unsignedData?.age ?: 0)
+ val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
event.prevContent
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 c854587853..30e1ec6679 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
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findAll
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -243,7 +244,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.stateKey == null || event.type == null) {
continue
}
- val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+ val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
Timber.v("## received state event ${event.type} and key ${event.stateKey}")
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
@@ -305,7 +306,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.stateKey == null || event.type == null) {
return@forEach
}
- val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+ val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = eventEntity.eventId
@@ -335,7 +336,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.stateKey == null || event.type == null) {
continue
}
- val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+ val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId
@@ -347,7 +348,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.senderId == null || event.type == null) {
continue
}
- val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+ val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
@@ -381,12 +382,13 @@ internal class RoomSyncHandler @Inject constructor(
aggregator: SyncResponsePostTreatmentAggregator
): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
- if (isLimited && lastChunk != null) {
- lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
- }
val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk
} else {
+ // Delete all chunks of the room in case of gap.
+ ChunkEntity.findAll(realm, roomId).forEach {
+ it.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
+ }
realm.createObject().apply {
this.prevToken = prevToken
this.isLastForward = true
@@ -399,7 +401,10 @@ internal class RoomSyncHandler @Inject constructor(
for (rawEvent in eventList) {
// It's annoying roomId is not there, but lot of code rely on it.
// And had to do it now as copy would delete all decryption results..
- val event = rawEvent.copy(roomId = roomId)
+ val ageLocalTs = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0)
+ val event = rawEvent.copy(roomId = roomId).also {
+ it.ageLocalTs = ageLocalTs
+ }
if (event.eventId == null || event.senderId == null || event.type == null) {
continue
}
@@ -421,7 +426,6 @@ internal class RoomSyncHandler @Inject constructor(
contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
}
- val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index 8c7557a5b8..70553359ff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
/**
@@ -64,7 +65,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private val permalinkFactory: PermalinkFactory,
@SessionDatabase private val monarchy: Monarchy,
private val lightweightSettingsStorage: LightweightSettingsStorage,
- private val getEventTask: GetEventTask
+ private val getEventTask: GetEventTask,
+ private val clock: Clock,
) {
// This caching is responsible to improve the performance when we receive a root event
@@ -120,7 +122,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private suspend fun fetchThreadsEvents(threadsToFetch: Map) {
val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) ->
fetchEvent(eventId, roomId)?.let {
- it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs)
+ it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs ?: clock.epochMillis())
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
index 396d12f369..8c7d7704ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.util.system
import dagger.Binds
import dagger.Module
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
+import org.matrix.android.sdk.internal.securestorage.DefaultSecureStorageService
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.util.time.DefaultClock
@@ -25,7 +27,7 @@ import org.matrix.android.sdk.internal.util.time.DefaultClock
internal abstract class SystemModule {
@Binds
- abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
+ abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
@Binds
abstract fun bindClock(clock: DefaultClock): Clock
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
index 32b1d44fb9..dac33069f3 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -23,10 +23,12 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.model.PusherEntity
+import org.matrix.android.sdk.internal.database.model.PusherEntityFields
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakePushersAPI
import org.matrix.android.sdk.test.fakes.FakeRequestExecutor
+import org.matrix.android.sdk.test.fakes.givenEqualTo
import java.net.SocketException
private val A_JSON_PUSHER = JsonPusher(
@@ -56,6 +58,7 @@ class DefaultAddPusherTaskTest {
@Test
fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
monarchy.givenWhereReturns(result = null)
+ .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
@@ -71,6 +74,7 @@ class DefaultAddPusherTaskTest {
fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() {
val realmResult = PusherEntity(appDisplayName = null)
monarchy.givenWhereReturns(result = realmResult)
+ .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
@@ -84,6 +88,7 @@ class DefaultAddPusherTaskTest {
fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() {
val realmResult = PusherEntity()
monarchy.givenWhereReturns(result = realmResult)
+ .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith {
@@ -96,6 +101,7 @@ class DefaultAddPusherTaskTest {
@Test
fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() {
monarchy.givenWhereReturns(result = null)
+ .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith {
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
new file mode 100644
index 0000000000..933087af2b
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
@@ -0,0 +1,405 @@
+/*
+ * 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 androidx.work.ExistingWorkPolicy
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+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.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindAll
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+import org.matrix.android.sdk.test.fakes.givenNotEqualTo
+
+private const val A_SESSION_ID = "session_id"
+private const val A_SENDER_ID = "sender_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_ROOM_ID = "room_id"
+private const val A_TIMESTAMP = 1654689143L
+private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L
+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;u=$A_UNCERTAINTY"
+
+internal class LiveLocationAggregationProcessorTest {
+
+ private val fakeWorkManagerProvider = FakeWorkManagerProvider()
+ private val fakeClock = FakeClock()
+ private val fakeRealm = FakeRealm()
+ private val fakeQuery = fakeRealm.givenWhere()
+
+ private val liveLocationAggregationProcessor = LiveLocationAggregationProcessor(
+ sessionId = A_SESSION_ID,
+ workManagerProvider = fakeWorkManagerProvider.instance,
+ clock = fakeClock
+ )
+
+ @Test
+ fun `given beacon info when it is local echo then it is ignored`() {
+ val event = Event(senderId = A_SENDER_ID)
+ val beaconInfo = MessageBeaconInfoContent()
+
+ val result = liveLocationAggregationProcessor.handleBeaconInfo(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconInfo,
+ roomId = A_ROOM_ID,
+ isLocalEcho = true
+ )
+
+ result shouldBeEqualTo false
+ }
+
+ private data class IgnoredBeaconInfoEvent(
+ val event: Event,
+ val beaconInfo: MessageBeaconInfoContent
+ )
+
+ @Test
+ fun `given beacon info and event when some values are missing then it is ignored`() {
+ val ignoredInfoEvents = listOf(
+ // missing senderId
+ IgnoredBeaconInfoEvent(
+ event = Event(eventId = AN_EVENT_ID, senderId = null),
+ beaconInfo = MessageBeaconInfoContent()
+ ),
+ // empty senderId
+ IgnoredBeaconInfoEvent(
+ event = Event(eventId = AN_EVENT_ID, senderId = ""),
+ beaconInfo = MessageBeaconInfoContent()
+ ),
+ // beacon is live and no eventId
+ IgnoredBeaconInfoEvent(
+ event = Event(eventId = null, senderId = A_SENDER_ID),
+ beaconInfo = MessageBeaconInfoContent(isLive = true)
+ ),
+ // beacon is live and eventId is empty
+ IgnoredBeaconInfoEvent(
+ event = Event(eventId = "", senderId = A_SENDER_ID),
+ beaconInfo = MessageBeaconInfoContent(isLive = true)
+ ),
+ // beacon is not live and replaced event id is null
+ IgnoredBeaconInfoEvent(
+ event = Event(
+ eventId = AN_EVENT_ID,
+ senderId = A_SENDER_ID,
+ unsignedData = UnsignedData(
+ age = 123,
+ replacesState = null
+ )
+ ),
+ beaconInfo = MessageBeaconInfoContent(isLive = false)
+ ),
+ // beacon is not live and replaced event id is empty
+ IgnoredBeaconInfoEvent(
+ event = Event(
+ eventId = AN_EVENT_ID,
+ senderId = A_SENDER_ID,
+ unsignedData = UnsignedData(
+ age = 123,
+ replacesState = ""
+ )
+ ),
+ beaconInfo = MessageBeaconInfoContent(isLive = false)
+ ),
+ )
+
+ ignoredInfoEvents.forEach {
+ val result = liveLocationAggregationProcessor.handleBeaconInfo(
+ realm = fakeRealm.instance,
+ event = it.event,
+ content = it.beaconInfo,
+ roomId = A_ROOM_ID,
+ isLocalEcho = false
+ )
+
+ result shouldBeEqualTo false
+ }
+ }
+
+ @Test
+ fun `given beacon info and existing entity when beacon content is correct and active then it is aggregated`() {
+ val event = Event(
+ senderId = A_SENDER_ID,
+ eventId = AN_EVENT_ID
+ )
+ val beaconInfo = MessageBeaconInfoContent(
+ isLive = true,
+ unstableTimestampMillis = A_TIMESTAMP,
+ timeout = A_TIMEOUT_MILLIS
+ )
+ fakeClock.givenEpoch(A_TIMESTAMP + 5000)
+ fakeWorkManagerProvider.fakeWorkManager.expectEnqueueUniqueWork()
+ val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+ val previousEntities = givenActiveSummaryListQueryReturns(
+ listOf(
+ LiveLocationShareAggregatedSummaryEntity(
+ eventId = "${AN_EVENT_ID}1",
+ roomId = A_ROOM_ID,
+ userId = A_SENDER_ID,
+ isActive = true
+ )
+ )
+ )
+
+ val result = liveLocationAggregationProcessor.handleBeaconInfo(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconInfo,
+ roomId = A_ROOM_ID,
+ isLocalEcho = false
+ )
+
+ result shouldBeEqualTo true
+ aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID
+ aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
+ aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
+ aggregatedEntity.isActive shouldBeEqualTo true
+ aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
+ aggregatedEntity.lastLocationContent shouldBeEqualTo null
+ previousEntities.forEach { entity ->
+ entity.isActive shouldBeEqualTo false
+ }
+ fakeWorkManagerProvider.fakeWorkManager.verifyEnqueueUniqueWork(
+ workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID),
+ policy = ExistingWorkPolicy.REPLACE
+ )
+ }
+
+ @Test
+ fun `given beacon info and existing entity when beacon content is correct and inactive then it is aggregated`() {
+ val unsignedData = UnsignedData(
+ age = 123,
+ replacesState = AN_EVENT_ID
+ )
+ val event = Event(
+ senderId = A_SENDER_ID,
+ eventId = "",
+ unsignedData = unsignedData
+ )
+ val beaconInfo = MessageBeaconInfoContent(
+ isLive = false,
+ unstableTimestampMillis = A_TIMESTAMP,
+ timeout = A_TIMEOUT_MILLIS
+ )
+ fakeClock.givenEpoch(A_TIMESTAMP + 5000)
+ fakeWorkManagerProvider.fakeWorkManager.expectCancelUniqueWork()
+ val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+ val previousEntities = givenActiveSummaryListQueryReturns(
+ listOf(
+ LiveLocationShareAggregatedSummaryEntity(
+ eventId = "${AN_EVENT_ID}1",
+ roomId = A_ROOM_ID,
+ userId = A_SENDER_ID,
+ isActive = true
+ )
+ )
+
+ )
+
+ val result = liveLocationAggregationProcessor.handleBeaconInfo(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconInfo,
+ roomId = A_ROOM_ID,
+ isLocalEcho = false
+ )
+
+ result shouldBeEqualTo true
+ aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID
+ aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
+ aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
+ aggregatedEntity.isActive shouldBeEqualTo false
+ aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
+ aggregatedEntity.lastLocationContent shouldBeEqualTo null
+ previousEntities.forEach { entity ->
+ entity.isActive shouldBeEqualTo false
+ }
+ fakeWorkManagerProvider.fakeWorkManager.verifyCancelUniqueWork(
+ workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+ )
+ }
+
+ @Test
+ fun `given beacon location data when it is local echo then it is ignored`() {
+ val event = Event(senderId = A_SENDER_ID)
+ val beaconLocationData = MessageBeaconLocationDataContent()
+
+ val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconLocationData,
+ roomId = A_ROOM_ID,
+ relatedEventId = AN_EVENT_ID,
+ isLocalEcho = true
+ )
+
+ result shouldBeEqualTo false
+ }
+
+ private data class IgnoredBeaconLocationDataEvent(
+ val event: Event,
+ val beaconLocationData: MessageBeaconLocationDataContent
+ )
+
+ @Test
+ fun `given event and beacon location data when some values are missing then it is ignored`() {
+ val ignoredLocationDataEvents = listOf(
+ // missing sender id
+ IgnoredBeaconLocationDataEvent(
+ event = Event(eventId = AN_EVENT_ID),
+ beaconLocationData = MessageBeaconLocationDataContent()
+ ),
+ // empty sender id
+ IgnoredBeaconLocationDataEvent(
+ event = Event(eventId = AN_EVENT_ID, senderId = ""),
+ beaconLocationData = MessageBeaconLocationDataContent()
+ ),
+ )
+
+ ignoredLocationDataEvents.forEach {
+ val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm = fakeRealm.instance,
+ event = it.event,
+ content = it.beaconLocationData,
+ roomId = A_ROOM_ID,
+ relatedEventId = "",
+ isLocalEcho = false
+ )
+ result shouldBeEqualTo false
+ }
+ }
+
+ @Test
+ fun `given beacon location data when relatedEventId is null or empty then it is ignored`() {
+ val event = Event(senderId = A_SENDER_ID)
+ val beaconLocationData = MessageBeaconLocationDataContent()
+
+ listOf(null, "").forEach {
+ val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconLocationData,
+ roomId = A_ROOM_ID,
+ relatedEventId = it,
+ isLocalEcho = false
+ )
+ result shouldBeEqualTo false
+ }
+ }
+
+ @Test
+ fun `given beacon location data when location is less recent than the saved one then it is ignored`() {
+ val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID)
+ val beaconLocationData = MessageBeaconLocationDataContent(
+ unstableTimestampMillis = A_TIMESTAMP - 60_000
+ )
+ val lastBeaconLocationContent = MessageBeaconLocationDataContent(
+ unstableTimestampMillis = A_TIMESTAMP
+ )
+ givenLastSummaryQueryReturns(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ beaconLocationContent = lastBeaconLocationContent
+ )
+
+ val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconLocationData,
+ roomId = A_ROOM_ID,
+ relatedEventId = AN_EVENT_ID,
+ isLocalEcho = false
+ )
+
+ result shouldBeEqualTo false
+ }
+
+ @Test
+ fun `given beacon location data when location is more recent than the saved one then it is aggregated`() {
+ val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID)
+ val locationInfo = LocationInfo(geoUri = A_GEO_URI)
+ val beaconLocationData = MessageBeaconLocationDataContent(
+ unstableTimestampMillis = A_TIMESTAMP,
+ unstableLocationInfo = locationInfo
+ )
+ val lastBeaconLocationContent = MessageBeaconLocationDataContent(
+ unstableTimestampMillis = A_TIMESTAMP - 60_000
+ )
+ val entity = givenLastSummaryQueryReturns(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ beaconLocationContent = lastBeaconLocationContent
+ )
+
+ val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm = fakeRealm.instance,
+ event = event,
+ content = beaconLocationData,
+ roomId = A_ROOM_ID,
+ relatedEventId = AN_EVENT_ID,
+ isLocalEcho = false
+ )
+
+ result shouldBeEqualTo true
+ val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel()
+ savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
+ savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
+ }
+
+ private fun givenLastSummaryQueryReturns(
+ eventId: String,
+ roomId: String,
+ beaconLocationContent: MessageBeaconLocationDataContent? = null
+ ): LiveLocationShareAggregatedSummaryEntity {
+ val result = LiveLocationShareAggregatedSummaryEntity(
+ eventId = eventId,
+ roomId = roomId,
+ lastLocationContent = ContentMapper.map(beaconLocationContent?.toContent())
+ )
+ fakeQuery
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
+ .givenFindFirst(result)
+ return result
+ }
+
+ private fun givenActiveSummaryListQueryReturns(
+ summaryList: List
+ ): List {
+ fakeQuery
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+ .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID)
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+ .givenFindAll(summaryList)
+ return summaryList
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
index 837bbeea26..3044ca5d43 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
@@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.mockk.every
import io.mockk.mockk
import io.realm.RealmList
-import io.realm.RealmModel
-import io.realm.RealmQuery
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.Before
@@ -46,6 +44,8 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
class PollAggregationProcessorTest {
@@ -135,14 +135,11 @@ class PollAggregationProcessorTest {
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
}
- private inline fun RealmQuery.givenEqualTo(fieldName: String, value: String, result: RealmQuery) {
- every { equalTo(fieldName, value) } returns result
- }
-
private fun mockEventAnnotationsSummaryEntity() {
- val queryResult = realm.givenWhereReturns(result = EventAnnotationsSummaryEntity())
- queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!, queryResult)
- queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!, queryResult)
+ realm.givenWhere()
+ .givenFindFirst(EventAnnotationsSummaryEntity())
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!)
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!)
}
private fun mockRoom(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt
new file mode 100644
index 0000000000..3198392eab
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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 io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultCheckIfExistingActiveLiveTaskTest {
+
+ private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
+
+ private val defaultCheckIfExistingActiveLiveTask = DefaultCheckIfExistingActiveLiveTask(
+ getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters and existing active live event when calling the task then result is true`() = runTest {
+ val params = CheckIfExistingActiveLiveTask.Params(
+ roomId = A_ROOM_ID
+ )
+ val currentStateEvent = Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+
+ val result = defaultCheckIfExistingActiveLiveTask.execute(params)
+
+ result shouldBeEqualTo true
+ val expectedGetActiveBeaconParams = GetActiveBeaconInfoForUserTask.Params(
+ roomId = params.roomId
+ )
+ fakeGetActiveBeaconInfoForUserTask.verifyExecute(expectedGetActiveBeaconParams)
+ }
+
+ @Test
+ fun `given parameters and no existing active live event when calling the task then result is false`() = runTest {
+ val params = CheckIfExistingActiveLiveTask.Params(
+ roomId = A_ROOM_ID
+ )
+ val inactiveEvents = listOf(
+ // no event
+ null,
+ // null content
+ Event(
+ stateKey = A_USER_ID,
+ content = null
+ ),
+ // inactive live
+ Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = false,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ )
+
+ inactiveEvents.forEach { currentStateEvent ->
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+
+ val result = defaultCheckIfExistingActiveLiveTask.execute(params)
+
+ result shouldBeEqualTo false
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
new file mode 100644
index 0000000000..588bfaa979
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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 io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+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.toContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultGetActiveBeaconInfoForUserTaskTest {
+
+ private val fakeStateEventDataSource = FakeStateEventDataSource()
+
+ private val defaultGetActiveBeaconInfoForUserTask = DefaultGetActiveBeaconInfoForUserTask(
+ userId = A_USER_ID,
+ stateEventDataSource = fakeStateEventDataSource.instance
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters and no error when calling the task then result is computed`() = runTest {
+ val currentStateEvent = Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
+ val params = GetActiveBeaconInfoForUserTask.Params(
+ roomId = A_ROOM_ID
+ )
+
+ val result = defaultGetActiveBeaconInfoForUserTask.execute(params)
+
+ result shouldBeEqualTo currentStateEvent
+ fakeStateEventDataSource.verifyGetStateEvent(
+ roomId = params.roomId,
+ eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+ stateKey = A_USER_ID
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
new file mode 100644
index 0000000000..de91206531
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
@@ -0,0 +1,268 @@
+/*
+ * 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.arch.core.util.Function
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+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.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeMonarchy
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenIsNotEmpty
+import org.matrix.android.sdk.test.fakes.givenIsNotNull
+
+private const val A_ROOM_ID = "room_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 40.0
+private const val AN_UNCERTAINTY = 5.0
+private const val A_TIMEOUT = 15_000L
+
+@ExperimentalCoroutinesApi
+internal class DefaultLocationSharingServiceTest {
+
+ private val fakeMonarchy = FakeMonarchy()
+ private val sendStaticLocationTask = mockk()
+ private val sendLiveLocationTask = mockk()
+ private val startLiveLocationShareTask = mockk()
+ private val stopLiveLocationShareTask = mockk()
+ private val checkIfExistingActiveLiveTask = mockk()
+ private val fakeLiveLocationShareAggregatedSummaryMapper = mockk()
+
+ private val defaultLocationSharingService = DefaultLocationSharingService(
+ roomId = A_ROOM_ID,
+ monarchy = fakeMonarchy.instance,
+ sendStaticLocationTask = sendStaticLocationTask,
+ sendLiveLocationTask = sendLiveLocationTask,
+ startLiveLocationShareTask = startLiveLocationShareTask,
+ stopLiveLocationShareTask = stopLiveLocationShareTask,
+ checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
+ liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
+ )
+
+ @Before
+ fun setUp() {
+ mockkStatic("androidx.lifecycle.Transformations")
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `static location can be sent`() = runTest {
+ val isUserLocation = true
+ val cancelable = mockk()
+ coEvery { sendStaticLocationTask.execute(any()) } returns cancelable
+
+ val result = defaultLocationSharingService.sendStaticLocation(
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY,
+ isUserLocation = isUserLocation
+ )
+
+ result shouldBeEqualTo cancelable
+ val expectedParams = SendStaticLocationTask.Params(
+ roomId = A_ROOM_ID,
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY,
+ isUserLocation = isUserLocation,
+ )
+ coVerify { sendStaticLocationTask.execute(expectedParams) }
+ }
+
+ @Test
+ fun `live location can be sent`() = runTest {
+ val cancelable = mockk()
+ coEvery { sendLiveLocationTask.execute(any()) } returns cancelable
+
+ val result = defaultLocationSharingService.sendLiveLocation(
+ beaconInfoEventId = AN_EVENT_ID,
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY
+ )
+
+ result shouldBeEqualTo cancelable
+ val expectedParams = SendLiveLocationTask.Params(
+ roomId = A_ROOM_ID,
+ beaconInfoEventId = AN_EVENT_ID,
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY
+ )
+ coVerify { sendLiveLocationTask.execute(expectedParams) }
+ }
+
+ @Test
+ fun `given existing active live can be stopped when starting a live then the current live is stopped and the new live is started`() = runTest {
+ coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
+ coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
+ coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+ val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+ val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+ val expectedStopParams = StopLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
+ val expectedStartParams = StartLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID,
+ timeoutMillis = A_TIMEOUT
+ )
+ coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
+ }
+
+ @Test
+ fun `given existing active live cannot be stopped when starting a live then the result is failure`() = runTest {
+ coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
+ val error = Throwable()
+ coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
+
+ val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+ val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+ val expectedStopParams = StopLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
+ }
+
+ @Test
+ fun `given no existing active live when starting a live then the new live is started`() = runTest {
+ coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
+ coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+ val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+ val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+ val expectedStartParams = StartLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID,
+ timeoutMillis = A_TIMEOUT
+ )
+ coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
+ }
+
+ @Test
+ fun `live location share can be stopped`() = runTest {
+ coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+ val result = defaultLocationSharingService.stopLiveLocationShare()
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+ val expectedParams = StopLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID
+ )
+ coVerify { stopLiveLocationShareTask.execute(expectedParams) }
+ }
+
+ @Test
+ fun `livedata of live summaries is correctly computed`() {
+ val entity = LiveLocationShareAggregatedSummaryEntity()
+ val summary = LiveLocationShareAggregatedSummary(
+ userId = "",
+ isActive = true,
+ endOfLiveTimestampMillis = 123,
+ lastLocationDataContent = null
+ )
+
+ fakeMonarchy.givenWhere()
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+ .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
+ .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
+ fakeMonarchy.givenFindAllMappedWithChangesReturns(
+ realmEntities = listOf(entity),
+ mappedResult = listOf(summary),
+ fakeLiveLocationShareAggregatedSummaryMapper
+ )
+
+ val result = defaultLocationSharingService.getRunningLiveLocationShareSummaries().value
+
+ result shouldBeEqualTo listOf(summary)
+ }
+
+ @Test
+ fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() {
+ val entity = LiveLocationShareAggregatedSummaryEntity()
+ val summary = LiveLocationShareAggregatedSummary(
+ userId = "",
+ isActive = true,
+ endOfLiveTimestampMillis = 123,
+ lastLocationDataContent = null
+ )
+
+ fakeMonarchy.givenWhere()
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+ .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
+ val liveData = fakeMonarchy.givenFindAllMappedWithChangesReturns(
+ realmEntities = listOf(entity),
+ mappedResult = listOf(summary),
+ fakeLiveLocationShareAggregatedSummaryMapper
+ )
+ val mapper = slot, Optional>>()
+ every {
+ Transformations.map(
+ liveData,
+ capture(mapper)
+ )
+ } answers {
+ val value = secondArg, Optional>>().apply(listOf(summary))
+ MutableLiveData(value)
+ }
+
+ val result = defaultLocationSharingService.getLiveLocationShareSummary(AN_EVENT_ID).value
+
+ result shouldBeEqualTo summary.toOptional()
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt
new file mode 100644
index 0000000000..423c680054
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
+import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
+
+private const val A_ROOM_ID = "room_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 44.0
+private const val AN_UNCERTAINTY = 5.0
+
+@ExperimentalCoroutinesApi
+internal class DefaultSendLiveLocationTaskTest {
+
+ private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
+ private val fakeEventSenderProcessor = FakeEventSenderProcessor()
+
+ private val defaultSendLiveLocationTask = DefaultSendLiveLocationTask(
+ localEchoEventFactory = fakeLocalEchoEventFactory.instance,
+ eventSenderProcessor = fakeEventSenderProcessor
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters when calling the task then it is correctly executed`() = runTest {
+ val params = SendLiveLocationTask.Params(
+ roomId = A_ROOM_ID,
+ beaconInfoEventId = AN_EVENT_ID,
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY
+ )
+ val event = fakeLocalEchoEventFactory.givenCreateLiveLocationEvent(
+ withLocalEcho = true
+ )
+ val cancelable = mockk()
+ fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
+
+ val result = defaultSendLiveLocationTask.execute(params)
+
+ result shouldBeEqualTo cancelable
+ fakeLocalEchoEventFactory.verifyCreateLiveLocationEvent(
+ roomId = params.roomId,
+ beaconInfoEventId = params.beaconInfoEventId,
+ latitude = params.latitude,
+ longitude = params.longitude,
+ uncertainty = params.uncertainty
+ )
+ fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt
new file mode 100644
index 0000000000..cfde568b71
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
+import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
+
+private const val A_ROOM_ID = "room_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 44.0
+private const val AN_UNCERTAINTY = 5.0
+
+@ExperimentalCoroutinesApi
+internal class DefaultSendStaticLocationTaskTest {
+
+ private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
+ private val fakeEventSenderProcessor = FakeEventSenderProcessor()
+
+ private val defaultSendStaticLocationTask = DefaultSendStaticLocationTask(
+ localEchoEventFactory = fakeLocalEchoEventFactory.instance,
+ eventSenderProcessor = fakeEventSenderProcessor
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters when calling the task then it is correctly executed`() = runTest {
+ val params = SendStaticLocationTask.Params(
+ roomId = A_ROOM_ID,
+ latitude = A_LATITUDE,
+ longitude = A_LONGITUDE,
+ uncertainty = AN_UNCERTAINTY,
+ isUserLocation = true
+ )
+ val event = fakeLocalEchoEventFactory.givenCreateStaticLocationEvent(
+ withLocalEcho = true
+ )
+ val cancelable = mockk()
+ fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
+
+ val result = defaultSendStaticLocationTask.execute(params)
+
+ result shouldBeEqualTo cancelable
+ fakeLocalEchoEventFactory.verifyCreateStaticLocationEvent(
+ roomId = params.roomId,
+ latitude = params.latitude,
+ longitude = params.longitude,
+ uncertainty = params.uncertainty,
+ isUserLocation = params.isUserLocation
+ )
+ fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
new file mode 100644
index 0000000000..909ba5d048
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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 io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeSendStateTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+internal class DefaultStartLiveLocationShareTaskTest {
+
+ private val fakeClock = FakeClock()
+ private val fakeSendStateTask = FakeSendStateTask()
+
+ private val defaultStartLiveLocationShareTask = DefaultStartLiveLocationShareTask(
+ userId = A_USER_ID,
+ clock = fakeClock,
+ sendStateTask = fakeSendStateTask
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters and no error when calling the task then result is success`() = runTest {
+ val params = StartLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID,
+ timeoutMillis = A_TIMEOUT
+ )
+ fakeClock.givenEpoch(AN_EPOCH)
+ fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+
+ val result = defaultStartLiveLocationShareTask.execute(params)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+ val expectedBeaconContent = MessageBeaconInfoContent(
+ timeout = params.timeoutMillis,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ val expectedParams = SendStateTask.Params(
+ roomId = params.roomId,
+ stateKey = A_USER_ID,
+ eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+ body = expectedBeaconContent
+ )
+ fakeSendStateTask.verifyExecuteRetry(
+ params = expectedParams,
+ remainingRetry = 3
+ )
+ }
+
+ @Test
+ fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
+ val params = StartLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID,
+ timeoutMillis = A_TIMEOUT
+ )
+ fakeClock.givenEpoch(AN_EPOCH)
+ fakeSendStateTask.givenExecuteRetryReturns("")
+
+ val result = defaultStartLiveLocationShareTask.execute(params)
+
+ result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+ }
+
+ @Test
+ fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
+ val params = StartLiveLocationShareTask.Params(
+ roomId = A_ROOM_ID,
+ timeoutMillis = A_TIMEOUT
+ )
+ fakeClock.givenEpoch(AN_EPOCH)
+ val error = Throwable()
+ fakeSendStateTask.givenExecuteRetryThrows(error)
+
+ val result = defaultStartLiveLocationShareTask.execute(params)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
new file mode 100644
index 0000000000..1abf179ccf
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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 io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.After
+import org.junit.Test
+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.toContent
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.test.fakes.FakeSendStateTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultStopLiveLocationShareTaskTest {
+
+ private val fakeSendStateTask = FakeSendStateTask()
+ private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
+
+ private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask(
+ sendStateTask = fakeSendStateTask,
+ getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
+ )
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given parameters and no error when calling the task then result is success`() = runTest {
+ val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+ val currentStateEvent = Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+ fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+
+ val result = defaultStopLiveLocationShareTask.execute(params)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+ val expectedBeaconContent = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = false,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ val expectedSendParams = SendStateTask.Params(
+ roomId = params.roomId,
+ stateKey = A_USER_ID,
+ eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+ body = expectedBeaconContent
+ )
+ fakeSendStateTask.verifyExecuteRetry(
+ params = expectedSendParams,
+ remainingRetry = 3
+ )
+ val expectedGetBeaconParams = GetActiveBeaconInfoForUserTask.Params(
+ roomId = params.roomId
+ )
+ fakeGetActiveBeaconInfoForUserTask.verifyExecute(
+ expectedGetBeaconParams
+ )
+ }
+
+ @Test
+ fun `given parameters and an incorrect current state event when calling the task then result is failure`() = runTest {
+ val incorrectCurrentStateEvents = listOf(
+ // no event
+ null,
+ // no stateKey
+ Event(
+ stateKey = null,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ ),
+ // null content
+ Event(
+ stateKey = A_USER_ID,
+ content = null
+ )
+ )
+
+ incorrectCurrentStateEvents.forEach { currentStateEvent ->
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+ fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+ val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+
+ val result = defaultStopLiveLocationShareTask.execute(params)
+
+ result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+ }
+ }
+
+ @Test
+ fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
+ val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+ val currentStateEvent = Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+ fakeSendStateTask.givenExecuteRetryReturns("")
+
+ val result = defaultStopLiveLocationShareTask.execute(params)
+
+ result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+ }
+
+ @Test
+ fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
+ val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+ val currentStateEvent = Event(
+ stateKey = A_USER_ID,
+ content = MessageBeaconInfoContent(
+ timeout = A_TIMEOUT,
+ isLive = true,
+ unstableTimestampMillis = AN_EPOCH
+ ).toContent()
+ )
+ fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+ val error = Throwable()
+ fakeSendStateTask.givenExecuteRetryThrows(error)
+
+ val result = defaultStopLiveLocationShareTask.execute(params)
+
+ result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt
new file mode 100644
index 0000000000..febf94f4cf
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.util.time.Clock
+
+internal class FakeClock : Clock by mockk() {
+ fun givenEpoch(epoch: Long) {
+ every { epochMillis() } returns epoch
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
new file mode 100644
index 0000000000..fbdcf5bfd7
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+
+internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
+
+ fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
+ every { postEvent(event) } returns cancelable
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt
new file mode 100644
index 0000000000..dc4a48908a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
+
+internal class FakeGetActiveBeaconInfoForUserTask : GetActiveBeaconInfoForUserTask by mockk() {
+
+ fun givenExecuteReturns(event: Event?) {
+ coEvery { execute(any()) } returns event
+ }
+
+ fun verifyExecute(params: GetActiveBeaconInfoForUserTask.Params) {
+ coVerify { execute(params) }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
new file mode 100644
index 0000000000..50ec85f14a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.verify
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+
+internal class FakeLocalEchoEventFactory {
+
+ val instance = mockk()
+
+ fun givenCreateStaticLocationEvent(withLocalEcho: Boolean): Event {
+ val event = Event()
+ every {
+ instance.createStaticLocationEvent(
+ roomId = any(),
+ latitude = any(),
+ longitude = any(),
+ uncertainty = any(),
+ isUserLocation = any()
+ )
+ } returns event
+
+ if (withLocalEcho) {
+ every { instance.createLocalEcho(event) } just runs
+ }
+ return event
+ }
+
+ fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
+ val event = Event()
+ every {
+ instance.createLiveLocationEvent(
+ beaconInfoEventId = any(),
+ roomId = any(),
+ latitude = any(),
+ longitude = any(),
+ uncertainty = any()
+ )
+ } returns event
+
+ if (withLocalEcho) {
+ every { instance.createLocalEcho(event) } just runs
+ }
+ return event
+ }
+
+ fun verifyCreateStaticLocationEvent(
+ roomId: String,
+ latitude: Double,
+ longitude: Double,
+ uncertainty: Double?,
+ isUserLocation: Boolean
+ ) {
+ verify {
+ instance.createStaticLocationEvent(
+ roomId = roomId,
+ latitude = latitude,
+ longitude = longitude,
+ uncertainty = uncertainty,
+ isUserLocation = isUserLocation
+ )
+ }
+ }
+
+ fun verifyCreateLiveLocationEvent(
+ roomId: String,
+ beaconInfoEventId: String,
+ latitude: Double,
+ longitude: Double,
+ uncertainty: Double?
+ ) {
+ verify {
+ instance.createLiveLocationEvent(
+ roomId = roomId,
+ beaconInfoEventId = beaconInfoEventId,
+ latitude = latitude,
+ longitude = longitude,
+ uncertainty = uncertainty
+ )
+ }
+ }
+
+ fun verifyCreateLocalEcho(event: Event) {
+ verify { instance.createLocalEcho(event) }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
index 0a22ef8996..d77084fe3b 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
@@ -16,40 +16,65 @@
package org.matrix.android.sdk.test.fakes
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import com.zhuinden.monarchy.Monarchy
import io.mockk.MockKVerificationScope
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
-import io.mockk.verify
+import io.mockk.slot
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmQuery
-import io.realm.kotlin.where
import org.matrix.android.sdk.internal.util.awaitTransaction
internal class FakeMonarchy {
val instance = mockk()
- private val realm = mockk(relaxed = true)
+ private val fakeRealm = FakeRealm()
init {
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
coEvery {
instance.awaitTransaction(any Any>())
} coAnswers {
- secondArg Any>().invoke(realm)
+ secondArg Any>().invoke(fakeRealm.instance)
}
}
- inline fun givenWhereReturns(result: T?) {
- val queryResult = mockk>(relaxed = true)
- every { queryResult.findFirst() } returns result
- every { realm.where() } returns queryResult
+ inline fun givenWhere(): RealmQuery {
+ return fakeRealm.givenWhere()
+ }
+
+ inline fun givenWhereReturns(result: T?): RealmQuery {
+ return fakeRealm.givenWhere()
+ .givenFindFirst(result)
}
inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
- verify { realm.insertOrUpdate(verification()) }
+ fakeRealm.verifyInsertOrUpdate(verification)
+ }
+
+ inline fun givenFindAllMappedWithChangesReturns(
+ realmEntities: List,
+ mappedResult: List,
+ mapper: Monarchy.Mapper
+ ): LiveData> {
+ every { mapper.map(any()) } returns mockk()
+ val monarchyQuery = slot>()
+ val monarchyMapper = slot>()
+ val result = MutableLiveData(mappedResult)
+ every {
+ instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
+ } answers {
+ monarchyQuery.captured.createQuery(fakeRealm.instance)
+ realmEntities.forEach {
+ monarchyMapper.captured.map(it)
+ }
+ result
+ }
+ return result
}
}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
index c07f8e1873..0ebff87278 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
@@ -16,21 +16,84 @@
package org.matrix.android.sdk.test.fakes
+import io.mockk.MockKVerificationScope
import io.mockk.every
import io.mockk.mockk
+import io.mockk.verify
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmQuery
+import io.realm.RealmResults
import io.realm.kotlin.where
internal class FakeRealm {
val instance = mockk(relaxed = true)
- inline fun givenWhereReturns(result: T?): RealmQuery {
- val queryResult = mockk>()
- every { queryResult.findFirst() } returns result
- every { instance.where() } returns queryResult
- return queryResult
+ inline fun givenWhere(): RealmQuery {
+ val query = mockk>()
+ every { instance.where() } returns query
+ return query
+ }
+
+ inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
+ verify { instance.insertOrUpdate(verification()) }
}
}
+
+inline fun RealmQuery.givenFindFirst(
+ result: T?
+): RealmQuery {
+ every { findFirst() } returns result
+ return this
+}
+
+inline fun RealmQuery.givenFindAll(
+ result: List
+): RealmQuery {
+ val realmResults = mockk>()
+ result.forEachIndexed { index, t ->
+ every { realmResults[index] } returns t
+ }
+ every { realmResults.size } returns result.size
+ every { findAll() } returns realmResults
+ return this
+}
+
+inline fun RealmQuery.givenEqualTo(
+ fieldName: String,
+ value: String
+): RealmQuery {
+ every { equalTo(fieldName, value) } returns this
+ return this
+}
+
+inline fun RealmQuery.givenEqualTo(
+ fieldName: String,
+ value: Boolean
+): RealmQuery {
+ every { equalTo(fieldName, value) } returns this
+ return this
+}
+
+inline fun RealmQuery.givenNotEqualTo(
+ fieldName: String,
+ value: String
+): RealmQuery {
+ every { notEqualTo(fieldName, value) } returns this
+ return this
+}
+
+inline fun RealmQuery.givenIsNotEmpty(
+ fieldName: String
+): RealmQuery {
+ every { isNotEmpty(fieldName) } returns this
+ return this
+}
+
+inline fun RealmQuery.givenIsNotNull(
+ fieldName: String
+): RealmQuery {
+ every { isNotNull(fieldName) } returns this
+ return this
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt
new file mode 100644
index 0000000000..08a25be93e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+
+internal class FakeSendStateTask : SendStateTask by mockk() {
+
+ fun givenExecuteRetryReturns(eventId: String) {
+ coEvery { executeRetry(any(), any()) } returns eventId
+ }
+
+ fun givenExecuteRetryThrows(error: Throwable) {
+ coEvery { executeRetry(any(), any()) } throws error
+ }
+
+ fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) {
+ coVerify { executeRetry(params, remainingRetry) }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
new file mode 100644
index 0000000000..ca03316fa7
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+
+internal class FakeStateEventDataSource {
+
+ val instance: StateEventDataSource = mockk()
+
+ fun givenGetStateEventReturns(event: Event?) {
+ every {
+ instance.getStateEvent(
+ roomId = any(),
+ eventType = any(),
+ stateKey = any()
+ )
+ } returns event
+ }
+
+ fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
+ verify {
+ instance.getStateEvent(
+ roomId = roomId,
+ eventType = eventType,
+ stateKey = QueryStringValue.Equals(stateKey)
+ )
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt
new file mode 100644
index 0000000000..b29d015a43
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.test.fakes
+
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeWorkManager {
+
+ val instance = mockk()
+
+ fun expectEnqueueUniqueWork() {
+ every { instance.enqueueUniqueWork(any(), any(), any()) } returns mockk()
+ }
+
+ fun verifyEnqueueUniqueWork(workName: String, policy: ExistingWorkPolicy) {
+ verify { instance.enqueueUniqueWork(workName, policy, any()) }
+ }
+
+ fun expectCancelUniqueWork() {
+ every { instance.cancelUniqueWork(any()) } returns mockk()
+ }
+
+ fun verifyCancelUniqueWork(workName: String) {
+ verify { instance.cancelUniqueWork(workName) }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt
new file mode 100644
index 0000000000..51ff24c01d
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
+
+internal class FakeWorkManagerProvider(
+ val fakeWorkManager: FakeWorkManager = FakeWorkManager(),
+) {
+
+ val instance = mockk().also {
+ every { it.workManager } returns fakeWorkManager.instance
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt
new file mode 100644
index 0000000000..c118cf07a1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.util
+
+import android.os.Build
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
+
+class DefaultBuildVersionSdkIntProviderTests {
+
+ @Test
+ fun getReturnsCurrentVersionFromBuild_Version_SDK_INT() {
+ val provider = DefaultBuildVersionSdkIntProvider()
+ provider.get() shouldBeEqualTo Build.VERSION.SDK_INT
+ }
+}
diff --git a/vector/build.gradle b/vector/build.gradle
index 8d704141e5..15b54a71d7 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -9,6 +9,10 @@ apply plugin: 'com.likethesalad.stem'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlinx-knit'
+if (project.hasProperty("coverage")) {
+ apply plugin: 'jacoco'
+}
+
kapt {
correctErrorTypes = true
}
@@ -31,7 +35,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 = 24
+ext.versionPatch = 26
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -119,8 +123,6 @@ ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].
def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android {
-
-
// Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use
// Ref: https://issuetracker.google.com/issues/144111441
ndkVersion "21.3.6528147"
@@ -145,7 +147,7 @@ android {
versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar"
// Generate a random app task affinity
- manifestPlaceholders = [appTaskAffinitySuffix:"H_${gitRevision()}"]
+ manifestPlaceholders = [appTaskAffinitySuffix: "H_${gitRevision()}"]
buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\""
@@ -244,7 +246,10 @@ android {
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
signingConfig signingConfigs.debug
- testCoverageEnabled true
+
+ if (project.hasProperty("coverage")) {
+ testCoverageEnabled = coverage.enableTestCoverage
+ }
}
release {
@@ -361,12 +366,12 @@ dependencies {
implementation libs.androidx.core
implementation "androidx.media:media:1.6.0"
implementation "androidx.transition:transition:1.4.1"
+ implementation libs.androidx.biometric
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
implementation libs.squareup.moshi
- implementation libs.squareup.moshiKt
kapt libs.squareup.moshiKotlin
// Lifecycle
@@ -384,7 +389,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
- implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.50'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51'
// FlowBinding
implementation libs.github.flowBinding
@@ -421,7 +426,6 @@ dependencies {
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation libs.androidx.autoFill
implementation 'jp.wasabeef:glide-transformations:4.3.0'
- implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
implementation 'com.github.hyuwah:DraggableView:1.0.0'
// Custom Tab
@@ -465,7 +469,7 @@ dependencies {
// UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:2.0.0'
// UnifiedPush gplay flavor only
- gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.0.0') {
+ gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.1') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ -529,14 +533,14 @@ dependencies {
}
// Flipper, debug builds only
- debugImplementation('com.facebook.flipper:flipper:0.149.0') {
+ debugImplementation(libs.flipper.flipper) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
- debugImplementation('com.facebook.flipper:flipper-network-plugin:0.149.0') {
+ debugImplementation(libs.flipper.flipperNetworkPlugin) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
- debugImplementation 'com.facebook.soloader:soloader:0.10.3'
- debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.1.0"
+ debugImplementation 'com.facebook.soloader:soloader:0.10.4'
+ debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
// Activate when you want to check for leaks, from time to time.
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
@@ -561,4 +565,5 @@ dependencies {
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
+ debugImplementation libs.androidx.fragmentTesting
}
diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
index 1399d1d6a9..7920e8e0d8 100644
--- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
@@ -29,6 +29,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import im.vector.app.features.MainActivity
+import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
import im.vector.app.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not
import org.junit.Ignore
@@ -106,6 +107,12 @@ class RegistrationTest {
.check(matches(isEnabled()))
.perform(closeSoftKeyboard(), click())
+ withIdlingResource(activityIdlingResource(AnalyticsOptInActivity::class.java)) {
+ onView(withId(R.id.later))
+ .check(matches(isDisplayed()))
+ .perform(click())
+ }
+
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
diff --git a/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt
new file mode 100644
index 0000000000..ddf89b5e46
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+
+class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
+ var value: Int = 0
+
+ override fun get() = value
+}
diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt b/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt
new file mode 100644
index 0000000000..16abada04c
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/core/utils/WaitUntil.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.utils
+
+import kotlinx.coroutines.delay
+import org.amshove.kluent.fail
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Tries a [condition] several times until it returns true or a [timeout] is reached waiting for some [retryDelay] time between retries.
+ * On timeout it fails with an [errorMessage].
+ */
+suspend fun waitUntilCondition(
+ errorMessage: String,
+ timeout: Duration = 1.seconds,
+ retryDelay: Duration = 50.milliseconds,
+ condition: () -> Boolean,
+) {
+ val start = System.currentTimeMillis()
+ do {
+ if (condition()) return
+ delay(retryDelay.inWholeMilliseconds)
+ } while (System.currentTimeMillis() - start < timeout.inWholeMilliseconds)
+ fail(errorMessage)
+}
+
+/**
+ * Tries a [block] several times until it runs with no errors or a [timeout] is reached waiting for some [retryDelay] time between retries.
+ * On timeout it fails with a custom [errorMessage] or a caught [AssertionError].
+ */
+suspend fun waitUntil(
+ errorMessage: String? = null,
+ timeout: Duration = 1.seconds,
+ retryDelay: Duration = 50.milliseconds,
+ block: () -> Unit,
+) {
+ var error: AssertionError?
+ val start = System.currentTimeMillis()
+ do {
+ try {
+ block()
+ return
+ } catch (e: AssertionError) {
+ error = e
+ }
+ delay(retryDelay.inWholeMilliseconds)
+ } while (System.currentTimeMillis() - start < timeout.inWholeMilliseconds)
+ if (errorMessage != null) {
+ fail(errorMessage)
+ } else {
+ throw error!!
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt
index 428efdea86..0c8aa95ee4 100644
--- a/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt
@@ -25,9 +25,11 @@ import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.text.style.UnderlineSpan
import androidx.emoji2.text.EmojiCompat
+import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.InstrumentedTest
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeTrue
+import org.junit.BeforeClass
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
@@ -42,6 +44,14 @@ import java.util.concurrent.TimeUnit
@Ignore
class SpanUtilsTest : InstrumentedTest {
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ fun setupClass() {
+ EmojiCompat.init(InstrumentationRegistry.getInstrumentation().targetContext)
+ }
+ }
+
private val spanUtils = SpanUtils {
val emojiCompat = EmojiCompat.get()
emojiCompat.waitForInit()
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt
new file mode 100644
index 0000000000..21e15e1585
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.pin.lockscreen
+
+object LockScreenTestConstants {
+ const val ALIAS = "some_alias"
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt
new file mode 100644
index 0000000000..b519d2f623
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.pin.lockscreen.biometrics
+
+import android.content.Intent
+import android.os.Build
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
+import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
+import androidx.lifecycle.lifecycleScope
+import androidx.test.core.app.ActivityScenario
+import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.TestBuildVersionSdkIntProvider
+import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
+import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
+import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
+import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
+import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity
+import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
+import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import io.mockk.unmockkObject
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class BiometricHelperTests {
+
+ private val biometricManager = mockk(relaxed = true)
+ private val lockScreenKeyRepository = mockk(relaxed = true)
+ private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
+
+ @Before
+ fun setup() {
+ clearAllMocks()
+ }
+
+ @Test
+ fun canUseWeakBiometricAuthReturnsTrueIfIsFaceUnlockEnabledAndCanAuthenticate() {
+ every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_SUCCESS
+ val configuration = createDefaultConfiguration(isFaceUnlockEnabled = true)
+ val biometricUtils = createBiometricHelper(configuration)
+
+ biometricUtils.canUseWeakBiometricAuth.shouldBeTrue()
+
+ val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isFaceUnlockEnabled = false))
+ biometricUtilsWithDisabledAuth.canUseWeakBiometricAuth.shouldBeFalse()
+
+ every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_ERROR_NONE_ENROLLED
+ biometricUtils.canUseWeakBiometricAuth.shouldBeFalse()
+ }
+
+ @Test
+ fun canUseStrongBiometricAuthReturnsTrueIfIsBiometricsEnabledAndCanAuthenticate() {
+ every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_SUCCESS
+ val configuration = createDefaultConfiguration(isBiometricsEnabled = true)
+ val biometricUtils = createBiometricHelper(configuration)
+
+ biometricUtils.canUseStrongBiometricAuth.shouldBeTrue()
+
+ val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = false))
+ biometricUtilsWithDisabledAuth.canUseStrongBiometricAuth.shouldBeFalse()
+
+ every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_ERROR_NONE_ENROLLED
+ biometricUtils.canUseStrongBiometricAuth.shouldBeFalse()
+ }
+
+ @Test
+ fun canUseDeviceCredentialAuthReturnsTrueIfIsDeviceCredentialsUnlockEnabledAndCanAuthenticate() {
+ every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_SUCCESS
+ val configuration = createDefaultConfiguration(isDeviceCredentialUnlockEnabled = true)
+ val biometricUtils = createBiometricHelper(configuration)
+
+ biometricUtils.canUseDeviceCredentialsAuth.shouldBeTrue()
+
+ val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isDeviceCredentialUnlockEnabled = false))
+ biometricUtilsWithDisabledAuth.canUseDeviceCredentialsAuth.shouldBeFalse()
+
+ every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_ERROR_NONE_ENROLLED
+ biometricUtils.canUseDeviceCredentialsAuth.shouldBeFalse()
+ }
+
+ @Test
+ fun isSystemAuthEnabledReturnsTrueIfAnyAuthenticationMethodIsAvailableAndEnabledAndSystemKeyExists() {
+ val biometricHelper = mockk(relaxed = true) {
+ every { hasSystemKey } returns true
+ every { isSystemKeyValid } returns true
+ every { canUseAnySystemAuth } answers { callOriginal() }
+ every { isSystemAuthEnabledAndValid } answers { callOriginal() }
+ }
+ biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse()
+
+ every { biometricHelper.canUseWeakBiometricAuth } returns true
+ biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
+
+ every { biometricHelper.canUseWeakBiometricAuth } returns false
+ every { biometricHelper.canUseStrongBiometricAuth } returns true
+ biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
+
+ every { biometricHelper.canUseStrongBiometricAuth } returns false
+ every { biometricHelper.canUseDeviceCredentialsAuth } returns true
+ biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
+
+ every { biometricHelper.isSystemKeyValid } returns false
+ biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse()
+ }
+
+ @Test
+ fun hasSystemKeyReturnsKeyHelperHasSystemKey() {
+ val biometricUtils = createBiometricHelper(createDefaultConfiguration())
+ every { lockScreenKeyRepository.hasSystemKey() } returns true
+ biometricUtils.hasSystemKey.shouldBeTrue()
+
+ every { lockScreenKeyRepository.hasSystemKey() } returns false
+ biometricUtils.hasSystemKey.shouldBeFalse()
+ }
+
+ @Test
+ fun isSystemKeyValidReturnsKeyHelperIsSystemKeyValid() {
+ val biometricUtils = createBiometricHelper(createDefaultConfiguration())
+ every { lockScreenKeyRepository.isSystemKeyValid() } returns true
+ biometricUtils.isSystemKeyValid.shouldBeTrue()
+
+ every { lockScreenKeyRepository.isSystemKeyValid() } returns false
+ biometricUtils.isSystemKeyValid.shouldBeFalse()
+ }
+
+ @Test
+ fun disableAuthenticationDeletesSystemKeyAndCancelsPrompt() {
+ val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration()))
+ biometricUtils.disableAuthentication()
+
+ verify { lockScreenKeyRepository.deleteSystemKey() }
+ verify { biometricUtils.cancelPrompt() }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Ignore("This won't work in CI as the emulator won't have biometric auth enabled.")
+ @Test
+ fun authenticateShowsPrompt() = runTest {
+ val biometricUtils = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))
+ every { lockScreenKeyRepository.isSystemKeyValid() } returns true
+ val latch = CountDownLatch(1)
+ with(ActivityScenario.launch(LockScreenTestActivity::class.java)) {
+ onActivity { activity ->
+ biometricUtils.authenticate(activity)
+ activity.supportFragmentManager.fragments.isNotEmpty().shouldBeTrue()
+ close()
+ latch.countDown()
+ }
+ }
+ latch.await(1, TimeUnit.SECONDS)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest {
+ mockkStatic("kotlinx.coroutines.flow.FlowKt")
+ val mockAuthChannel: Channel = mockk(relaxed = true) {
+ // Empty flow to keep the dialog open
+ every { receiveAsFlow() } returns flowOf()
+ }
+ val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
+ every { createAuthChannel() } returns mockAuthChannel
+ }
+ mockkObject(DevicePromptCheck)
+ every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true
+ every { lockScreenKeyRepository.isSystemKeyValid() } returns true
+ val latch = CountDownLatch(1)
+ val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
+ with(ActivityScenario.launch(intent)) {
+ onActivity { activity ->
+ biometricUtils.authenticate(activity)
+ launch {
+ activity.supportFragmentManager.fragments.any { it is FallbackBiometricDialogFragment }.shouldBeTrue()
+ close()
+ latch.countDown()
+ }
+ }
+ }
+ latch.await(1, TimeUnit.SECONDS)
+ unmockkObject(DevicePromptCheck)
+ unmockkStatic("kotlinx.coroutines.flow.FlowKt")
+ }
+
+ @Test
+ fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest {
+ buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+ every { lockScreenKeyRepository.isSystemKeyValid() } returns true
+ val mockAuthChannel = Channel(capacity = 1)
+ val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
+ every { createAuthChannel() } returns mockAuthChannel
+ every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
+ }
+
+ val latch = CountDownLatch(1)
+ val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
+ ActivityScenario.launch(intent).onActivity { activity ->
+ activity.lifecycleScope.launch {
+ launch {
+ mockAuthChannel.send(true)
+ mockAuthChannel.close()
+ }
+ biometricUtils.authenticate(activity).collect()
+ latch.countDown()
+ }
+ }
+
+ latch.await(1, TimeUnit.SECONDS)
+ verify { lockScreenKeyRepository.ensureSystemKey() }
+ }
+
+ private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val configProvider = LockScreenConfiguratorProvider(configuration)
+ return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
+ }
+
+ private fun createDefaultConfiguration(
+ mode: LockScreenMode = LockScreenMode.VERIFY,
+ pinCodeLength: Int = 4,
+ isBiometricsEnabled: Boolean = false,
+ isFaceUnlockEnabled: Boolean = false,
+ isDeviceCredentialUnlockEnabled: Boolean = false,
+ needsNewCodeValidation: Boolean = false,
+ otherChanges: LockScreenConfiguration.() -> LockScreenConfiguration = { this },
+ ): LockScreenConfiguration = LockScreenConfiguration(
+ mode,
+ pinCodeLength,
+ isBiometricsEnabled,
+ isFaceUnlockEnabled,
+ isDeviceCredentialUnlockEnabled,
+ needsNewCodeValidation
+ ).let(otherChanges)
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt
new file mode 100644
index 0000000000..68e1244791
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.pin.lockscreen.crypto
+
+import android.os.Build
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.TestBuildVersionSdkIntProvider
+import io.mockk.every
+import io.mockk.spyk
+import io.mockk.verify
+import org.amshove.kluent.invoking
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldThrow
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
+import java.security.KeyStore
+
+class KeyStoreCryptoTests {
+
+ private val alias = "some_alias"
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
+ private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
+ private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider))
+ private val keyStoreCrypto = spyk(
+ KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils)
+ )
+
+ @After
+ fun setup() {
+ keyStore.deleteEntry(alias)
+ }
+
+ @Test
+ fun ensureKeyChecksValidityOfKeyAndThrows() {
+ keyStore.containsAlias(alias) shouldBe false
+
+ val exception = KeyPermanentlyInvalidatedException()
+ every { secretStoringUtils.getEncryptCipher(any()) } throws exception
+
+ invoking { keyStoreCrypto.ensureKey() } shouldThrow exception
+ keyStoreCrypto.hasValidKey() shouldBe false
+ }
+
+ @Test
+ fun hasValidKeyChecksValidityOfKey() {
+ runCatching { keyStoreCrypto.ensureKey() }
+ keyStoreCrypto.hasValidKey() shouldBe true
+
+ val exception = KeyPermanentlyInvalidatedException()
+ every { secretStoringUtils.getEncryptCipher(any()) } throws exception
+
+ runCatching { keyStoreCrypto.ensureKey() }
+ keyStoreCrypto.hasValidKey() shouldBe false
+ }
+
+ @Test
+ fun hasKeyChecksIfKeyExists() {
+ keyStoreCrypto.hasKey() shouldBe false
+
+ keyStoreCrypto.ensureKey()
+ keyStoreCrypto.hasKey() shouldBe true
+ keyStore.containsAlias(keyStoreCrypto.alias)
+
+ keyStoreCrypto.deleteKey()
+ keyStoreCrypto.hasKey() shouldBe false
+ }
+
+ @Test
+ fun deleteKeyRemovesTheKey() {
+ keyStore.containsAlias(alias) shouldBe false
+
+ keyStoreCrypto.ensureKey()
+ keyStore.containsAlias(alias) shouldBe true
+
+ keyStoreCrypto.deleteKey()
+ keyStore.containsAlias(alias) shouldBe false
+ }
+
+ @Test
+ fun checkEncryptionAndDecryptionOfStringsWorkAsExpected() {
+ val original = "some plain text"
+ val encryptedString = keyStoreCrypto.encryptToString(original)
+ val encryptedBytes = keyStoreCrypto.encrypt(original)
+ val result = keyStoreCrypto.decryptToString(encryptedString)
+ val resultFromBytes = keyStoreCrypto.decryptToString(encryptedBytes)
+ result shouldBeEqualTo original
+ resultFromBytes shouldBeEqualTo original
+ }
+
+ @Test
+ fun checkEncryptionAndDecryptionWorkAsExpected() {
+ val original = "some plain text".toByteArray()
+ val encryptedBytes = keyStoreCrypto.encrypt(original)
+ val encryptedString = keyStoreCrypto.encryptToString(original)
+ val result = keyStoreCrypto.decrypt(encryptedBytes)
+ val resultFromString = keyStoreCrypto.decrypt(encryptedString)
+ result shouldBeEqualTo original
+ resultFromString shouldBeEqualTo original
+ }
+
+ @Test
+ fun hasValidKeyReturnsFalseWhenKeyPermanentlyInvalidatedExceptionIsThrown() {
+ every { keyStoreCrypto.hasKey() } returns true
+ every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
+
+ keyStoreCrypto.hasValidKey().shouldBeFalse()
+ }
+
+ @Test
+ fun hasValidKeyReturnsFalseWhenKeyDoesNotExist() {
+ every { keyStoreCrypto.hasKey() } returns false
+ keyStoreCrypto.hasValidKey().shouldBeFalse()
+ }
+
+ @Test
+ fun hasValidKeyReturnsIfKeyExistsOnAndroidL() {
+ versionProvider.value = Build.VERSION_CODES.LOLLIPOP
+
+ every { keyStoreCrypto.hasKey() } returns true
+ keyStoreCrypto.hasValidKey().shouldBeTrue()
+
+ every { keyStoreCrypto.hasKey() } returns false
+ keyStoreCrypto.hasValidKey().shouldBeFalse()
+ }
+
+ @Test
+ fun getCryptoObjectUsesCipherFromSecretStoringUtils() {
+ keyStoreCrypto.getCryptoObject()
+ verify { secretStoringUtils.getEncryptCipher(any()) }
+
+ every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
+ invoking { keyStoreCrypto.getCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt
new file mode 100644
index 0000000000..23eefe6577
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.pin.lockscreen.crypto
+
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.features.settings.VectorPreferences
+import io.mockk.clearAllMocks
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.coInvoking
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldNotThrow
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
+import java.security.KeyStore
+
+class LockScreenKeyRepositoryTests {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val buildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider()
+
+ private val keyStoreCryptoFactory: KeyStoreCrypto.Factory = mockk {
+ every { provide(any(), any()) } answers {
+ KeyStoreCrypto(arg(0), false, context, buildVersionSdkIntProvider, keyStore)
+ }
+ }
+
+ private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
+ private val pinCodeMigrator: PinCodeMigrator = mockk(relaxed = true)
+ private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
+
+ private val keyStore: KeyStore by lazy {
+ KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
+ }
+
+ @Before
+ fun setup() {
+ lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory))
+ }
+
+ @After
+ fun tearDown() {
+ clearAllMocks()
+ keyStore.deleteEntry("base.pin_code")
+ keyStore.deleteEntry("base.system")
+ }
+
+ @Test
+ fun ensureSystemKeyCreatesSystemKeyIfNeeded() {
+ lockScreenKeyRepository.ensureSystemKey()
+ lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
+ }
+
+ @Test
+ fun encryptPinCodeCreatesPinCodeKey() {
+ lockScreenKeyRepository.encryptPinCode("1234")
+ lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
+ }
+
+ @Test
+ fun decryptPinCodeDecryptsEncodedPinCode() {
+ val decodedPinCode = "1234"
+ val pinCodeKeyCryptoMock = mockk(relaxed = true) {
+ every { decryptToString(any()) } returns decodedPinCode
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns pinCodeKeyCryptoMock
+ lockScreenKeyRepository.decryptPinCode("SOME_VALUE") shouldBeEqualTo decodedPinCode
+ }
+
+ @Test
+ fun isSystemKeyValidReturnsWhatKeyStoreCryptoHasValidKeyReplies() {
+ val systemKeyCryptoMock = mockk(relaxed = true) {
+ every { hasKey() } returns true
+ }
+ every { keyStoreCryptoFactory.provide(any(), any()) } returns systemKeyCryptoMock
+
+ every { systemKeyCryptoMock.hasValidKey() } returns false
+ lockScreenKeyRepository.isSystemKeyValid().shouldBeFalse()
+
+ every { systemKeyCryptoMock.hasValidKey() } returns true
+ lockScreenKeyRepository.isSystemKeyValid().shouldBeTrue()
+ }
+
+ @Test
+ fun hasSystemKeyReturnsTrueAfterSystemKeyIsCreated() {
+ lockScreenKeyRepository.hasSystemKey().shouldBeFalse()
+
+ lockScreenKeyRepository.ensureSystemKey()
+
+ lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
+ }
+
+ @Test
+ fun hasPinCodeKeyReturnsTrueAfterPinCodeKeyIsCreated() {
+ lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
+
+ lockScreenKeyRepository.encryptPinCode("1234")
+
+ lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
+ }
+
+ @Test
+ fun deleteSystemKeyRemovesTheKeyFromKeyStore() {
+ lockScreenKeyRepository.ensureSystemKey()
+ lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
+
+ lockScreenKeyRepository.deleteSystemKey()
+
+ lockScreenKeyRepository.hasSystemKey().shouldBeFalse()
+ }
+
+ @Test
+ fun deletePinCodeKeyRemovesTheKeyFromKeyStore() {
+ lockScreenKeyRepository.encryptPinCode("1234")
+ lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
+
+ lockScreenKeyRepository.deletePinCodeKey()
+
+ lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
+ }
+
+ @Test
+ fun migrateKeysIfNeededReturnsEarlyIfNotNeeded() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns false
+
+ lockScreenKeyRepository.migrateKeysIfNeeded()
+
+ coVerify(exactly = 0) { pinCodeMigrator.migrate(any()) }
+ }
+
+ @Test
+ fun migrateKeysIfNeededWillMigratePinCodeAndKeys() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns true
+
+ lockScreenKeyRepository.migrateKeysIfNeeded()
+
+ coVerify { pinCodeMigrator.migrate(any()) }
+ }
+
+ @Test
+ fun migrateKeysIfNeededWillCreateSystemKeyIfNeeded() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns true
+ every { vectorPreferences.useBiometricsToUnlock() } returns true
+ every { lockScreenKeyRepository.ensureSystemKey() } returns mockk()
+
+ lockScreenKeyRepository.migrateKeysIfNeeded()
+
+ verify { lockScreenKeyRepository.ensureSystemKey() }
+ }
+
+ @Test
+ fun migrateKeysIfNeededWillHandleKeyPermanentlyInvalidatedException() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns true
+ every { vectorPreferences.useBiometricsToUnlock() } returns true
+ every { lockScreenKeyRepository.ensureSystemKey() } throws KeyPermanentlyInvalidatedException()
+
+ coInvoking { lockScreenKeyRepository.migrateKeysIfNeeded() } shouldNotThrow KeyPermanentlyInvalidatedException::class
+
+ verify { lockScreenKeyRepository.ensureSystemKey() }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt
new file mode 100644
index 0000000000..297793c7a4
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("DEPRECATION")
+
+package im.vector.app.features.pin.lockscreen.crypto
+
+import android.os.Build
+import android.security.KeyPairGeneratorSpec
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import androidx.preference.PreferenceManager
+import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.features.pin.PinCodeStore
+import im.vector.app.features.pin.SharedPrefPinCodeStore
+import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE
+import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+import java.math.BigInteger
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.spec.MGF1ParameterSpec
+import java.security.spec.X509EncodedKeySpec
+import java.util.Calendar
+import java.util.UUID
+import javax.crypto.Cipher
+import javax.crypto.spec.OAEPParameterSpec
+import javax.crypto.spec.PSource
+import javax.security.auth.x500.X500Principal
+import kotlin.math.abs
+
+class PinCodeMigratorTests {
+
+ private val alias = UUID.randomUUID().toString()
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val pinCodeStore: PinCodeStore = spyk(
+ SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context))
+ )
+ private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) }
+ private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk {
+ every { get() } returns Build.VERSION_CODES.M
+ }
+ private val secretStoringUtils: SecretStoringUtils = spyk(
+ SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
+ )
+ private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider))
+
+ @After
+ fun tearDown() {
+ if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) {
+ keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS)
+ }
+ if (keyStore.containsAlias(alias)) {
+ keyStore.deleteEntry(alias)
+ }
+ runBlocking { pinCodeStore.deletePinCode() }
+ }
+
+ @Test
+ fun isMigrationNeededReturnsTrueIfLegacyKeyExists() {
+ pinCodeMigrator.isMigrationNeeded() shouldBe false
+
+ generateLegacyKey()
+
+ pinCodeMigrator.isMigrationNeeded() shouldBe true
+ }
+
+ @Test
+ fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns false
+ coEvery { pinCodeStore.getPinCode() } returns null
+
+ pinCodeMigrator.migrate(alias)
+
+ coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() }
+ verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
+ coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
+ verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
+ }
+
+ @Test
+ fun migrateWillReturnEarlyIfIsNotNeeded() = runTest {
+ every { pinCodeMigrator.isMigrationNeeded() } returns false
+ coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234"
+ every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0)
+
+ pinCodeMigrator.migrate(alias)
+
+ coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() }
+ verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
+ coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
+ verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
+ }
+
+ @Test
+ fun migratePinCodeM() = runTest {
+ val pinCode = "1234"
+ saveLegacyPinCode(pinCode)
+
+ pinCodeMigrator.migrate(alias)
+
+ coVerify { pinCodeMigrator.getDecryptedPinCode() }
+ verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
+ coVerify { pinCodeStore.savePinCode(any()) }
+ verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
+
+ val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias))
+ decodedPinCode shouldBeEqualTo pinCode
+ keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false
+ keyStore.containsAlias(alias) shouldBe true
+ }
+
+ @Test
+ fun migratePinCodeL() = runTest {
+ val pinCode = "1234"
+ every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
+ saveLegacyPinCode(pinCode)
+
+ pinCodeMigrator.migrate(alias)
+
+ coVerify { pinCodeMigrator.getDecryptedPinCode() }
+ verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
+ coVerify { pinCodeStore.savePinCode(any()) }
+ verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
+
+ val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias))
+ decodedPinCode shouldBeEqualTo pinCode
+ keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false
+ keyStore.containsAlias(alias) shouldBe true
+ }
+
+ private fun generateLegacyKey() {
+ if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
+
+ if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
+ generateLegacyKeyM()
+ } else {
+ generateLegacyKeyL()
+ }
+ }
+
+ private fun generateLegacyKeyL() {
+ val start = Calendar.getInstance()
+ val end = Calendar.getInstance().also { it.add(Calendar.YEAR, 25) }
+
+ val keyGen = KeyPairGenerator
+ .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE)
+
+ val spec = KeyPairGeneratorSpec.Builder(context)
+ .setAlias(LEGACY_PIN_CODE_KEY_ALIAS)
+ .setSubject(X500Principal("CN=$LEGACY_PIN_CODE_KEY_ALIAS"))
+ .setSerialNumber(BigInteger.valueOf(abs(LEGACY_PIN_CODE_KEY_ALIAS.hashCode()).toLong()))
+ .setEndDate(end.time)
+ .setStartDate(start.time)
+ .setSerialNumber(BigInteger.ONE)
+ .setSubject(X500Principal("CN = Secured Preference Store, O = Devliving Online"))
+ .build()
+
+ keyGen.initialize(spec)
+ keyGen.generateKeyPair()
+ }
+
+ private fun generateLegacyKeyM() {
+ val keyGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE)
+ keyGenerator.initialize(
+ KeyGenParameterSpec.Builder(LEGACY_PIN_CODE_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
+ .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
+ .build()
+ )
+ keyGenerator.generateKeyPair()
+ }
+
+ private suspend fun saveLegacyPinCode(value: String) {
+ generateLegacyKey()
+ val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey
+ val cipher = getLegacyCipher()
+ if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
+ val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded))
+ val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT)
+ cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec)
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey)
+ }
+ val bytes = cipher.doFinal(value.toByteArray())
+ val encryptedPinCode = Base64.encodeToString(bytes, Base64.NO_WRAP)
+ pinCodeStore.savePinCode(encryptedPinCode)
+ }
+
+ private fun getLegacyCipher(): Cipher {
+ return when (buildVersionSdkIntProvider.get()) {
+ Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL()
+ else -> getCipherM()
+ }
+ }
+
+ private fun getCipherL(): Cipher {
+ val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround"
+ val transformation = "RSA/ECB/PKCS1Padding"
+ return Cipher.getInstance(transformation, provider)
+ }
+
+ private fun getCipherM(): Cipher {
+ val transformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
+ return Cipher.getInstance(transformation)
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt
new file mode 100644
index 0000000000..1545e140a0
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.pin.lockscreen.tests
+
+import androidx.fragment.app.FragmentActivity
+
+class LockScreenTestActivity : FragmentActivity()
diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt
new file mode 100644
index 0000000000..3781535f72
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.pin.lockscreen.ui.fallbackprompt
+
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.testing.launchFragment
+import androidx.lifecycle.Lifecycle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.airbnb.mvrx.Mavericks
+import im.vector.app.R
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+
+class FallbackBiometricDialogFragmentTests {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @Test
+ fun dismissTriggersOnDismissCallback() {
+ val latch = CountDownLatch(1)
+ val fragmentScenario = launchFragment(noArgsBundle())
+ fragmentScenario.onFragment { fragment ->
+ fragment.onDismiss = { latch.countDown() }
+ fragment.dismiss()
+ }
+ latch.await()
+ }
+
+ @Test
+ fun argsModifyUI() {
+ val latch = CountDownLatch(1)
+ val args = FallbackBiometricDialogFragment.Args(
+ title = "Title",
+ description = "Description",
+ cancelActionText = "Cancel text",
+ )
+ val fragmentScenario = launchFragment(bundleOf(Mavericks.KEY_ARG to args))
+ fragmentScenario.onFragment { fragment ->
+ val view = fragment.requireView()
+ view.findViewById