Merge branch 'develop' into feature/fre/start_dm_on_first_msg

* develop: (174 commits)
  Bump libphonenumber from 8.12.50 to 8.12.51
  LoadRoomMember: fix presence
  Cleanup
  LoadRoomMembers: add changelog
  LoadRoomMembers: handle room member event a bit more efficiently
  LoadRoomMembers: exclude Membership.Leave
  LoadRoomMembers: divide by chunk
  Bump soloader from 0.10.3 to 0.10.4
  Code review fix.
  Try no using the gradle daemon on CI
  Harmonize values of `CI_GRADLE_ARG_PROPERTIES`
  removing unused dependencies and marking soloader and ignored from dependency check (as it's dynamic)
  Remove non necessary prefix in logs
  Adding changelog entry
  Updating the unit tests
  Stopping existing active live when starting a new one
  Avoid multiple PR from Dependabot when Flipper is upgraded.
  Change context inside the get live summary use case
  Use a TestDispatcher in the FakeSession
  Code review fixes.
  ...
This commit is contained in:
Florian Renaud 2022-06-30 11:48:55 +02:00
commit 3f087eb632
289 changed files with 9514 additions and 1219 deletions

View file

@ -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:

View file

@ -13,6 +13,7 @@ env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
--no-daemon
jobs:

View file

@ -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

View file

@ -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:

2
.gitignore vendored
View file

@ -16,4 +16,4 @@
/fastlane/private
/fastlane/report.xml
/library/build
/**/build

View file

@ -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)
=======================================

View file

@ -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",

1
changelog.d/5821.bugfix Normal file
View file

@ -0,0 +1 @@
Fixes concurrent modification crash when signing out or launching the app

1
changelog.d/5864.sdk Normal file
View file

@ -0,0 +1 @@
Group all location sharing related API into LocationSharingService

1
changelog.d/6101.bugfix Normal file
View file

@ -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.

1
changelog.d/6155.misc Normal file
View file

@ -0,0 +1 @@
Add unit tests for LiveLocationAggregationProcessor code

1
changelog.d/6191.sdk Normal file
View file

@ -0,0 +1 @@
Add support for MSC2457 - opting in or out of logging out all devices when changing password

1
changelog.d/6217.feature Normal file
View file

@ -0,0 +1 @@
Improve lock screen implementation.

1
changelog.d/6315.bugfix Normal file
View file

@ -0,0 +1 @@
[Location sharing] Fix crash when starting/stopping a live when offline

1
changelog.d/6318.bugfix Normal file
View file

@ -0,0 +1 @@
Fix loop in timeline and simplify management of chunks and timeline events.

1
changelog.d/6326.bugfix Normal file
View file

@ -0,0 +1 @@
Update design and behaviour on widget permission bottom sheet

1
changelog.d/6328.bugfix Normal file
View file

@ -0,0 +1 @@
Fix | Some user verification requests couldn't be accepted/declined

1
changelog.d/6329.misc Normal file
View file

@ -0,0 +1 @@
Fix flaky test in voice recording feature.

1
changelog.d/6349.bugfix Normal file
View file

@ -0,0 +1 @@
[Location sharing] Fix stop of a live not possible from another device

1
changelog.d/6350.feature Normal file
View file

@ -0,0 +1 @@
Promote live location labs flag

1
changelog.d/6357.bugfix Normal file
View file

@ -0,0 +1 @@
Fix backslash escapes in formatted messages

1
changelog.d/6364.feature Normal file
View file

@ -0,0 +1 @@
[Location sharing] - Stop any active live before starting a new one

1
changelog.d/6366.misc Normal file
View file

@ -0,0 +1 @@
Poll view state unit tests

2
changelog.d/6369.feature Normal file
View file

@ -0,0 +1,2 @@
Expose pusher profile tag in advanced settings

1
changelog.d/6371.bugfix Normal file
View file

@ -0,0 +1 @@
Fixes wrong error message when signing in with wrong credentials

1
changelog.d/6375.bugfix Normal file
View file

@ -0,0 +1 @@
[Location Share] - Adding missing prefix "u=" for uncertainty in geo URI

1
changelog.d/6394.misc Normal file
View file

@ -0,0 +1 @@
Let LoadRoomMembersTask insert by chunk to release db.

1
changelog.d/6396.doc Normal file
View file

@ -0,0 +1 @@
Update the PR process doc to come back to one reviewer with optional additional reviewers.

View file

@ -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']
}

View file

@ -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"
]
]

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:type="linear"
android:startColor="#f28433"
android:endColor="#e0574c"
android:angle="270" />
</shape>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="#44FFFFFF"/>
<size
android:width="70dp"
android:height="70dp"/>
</shape>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:color="@color/lockscreen_code"
android:width="1px"/>
<size
android:width="@dimen/lockscreen_code_size"
android:height="@dimen/lockscreen_code_size"/>
</shape>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:color="@color/lockscreen_code"
android:width="1px"/>
<solid
android:color="@color/lockscreen_code"/>
<size
android:width="@dimen/lockscreen_code_size"
android:height="@dimen/lockscreen_code_size"/>
</shape>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<padding android:padding="1dp" />
<corners android:radius="5dp" />
<solid android:color="#44FFFFFF" />
</shape>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- NOTE: order is important (the first matching state(s) is what is rendered) -->
<item
android:state_checked="true"
android:drawable="@drawable/lockscreen_circle_code_fill"/>
<item
android:drawable="@drawable/lockscreen_circle_code_empty"/>
</selector>

View file

@ -0,0 +1,7 @@
<vector android:height="14.498462dp" android:viewportHeight="589"
android:viewportWidth="975" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:pathData="M951,24H302.88L34,294L302.88,565H951V24Z"
android:strokeColor="#000000" android:strokeWidth="48"/>
<path android:pathData="M411.5,120L757.5,467.5M757.5,120L411.5,467.5"
android:strokeColor="#000000" android:strokeWidth="48"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/lockscreen_white_selector">
<item android:id="@android:id/mask">
<shape android:shape="oval">
<padding android:padding="1dp" />
<corners android:radius="5dp" />
<solid android:color="@color/lockscreen_white_selector"/>
</shape>
</item>
<item>
<selector>
<item android:state_selected="true">
<color android:color="@android:color/darker_gray"/>
</item>
<item android:state_activated="true">
<color android:color="@android:color/white"/>
</item>
<item>
<color android:color="@android:color/transparent"/>
</item>
</selector>
</item>
</ripple>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_focused="true" android:state_enabled="false" android:state_pressed="true"
android:drawable="@drawable/lockscreen_circle_key_selector" />
<item android:state_focused="true" android:state_enabled="false"
android:drawable="@drawable/lockscreen_circle_key_selector" />
<item android:state_focused="true" android:state_pressed="true"
android:drawable="@drawable/lockscreen_circle_key_selector" />
<item android:state_focused="false" android:state_pressed="true"
android:drawable="@drawable/lockscreen_circle_key_selector" />
<item android:state_focused="true"
android:drawable="@drawable/lockscreen_circle_key_selector" />
<item
android:drawable="@drawable/lockscreen_circle_background" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="lockscreen_button_size">60dp</dimen>
<dimen name="lockscreen_button_margin_vertical">15dp</dimen>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="lockscreen_key_button_theme" format="reference|integer"/>
<attr name="lockscreen_theme" format="reference|integer"/>
<attr name="lockscreen_fingerprint_button_theme" format="reference|integer"/>
<attr name="lockscreen_delete_button_theme" format="reference|integer"/>
<attr name="lockscreen_code_view_theme" format="reference|integer"/>
<attr name="lockscreen_title_theme" format="reference|integer"/>
<attr name="lockscreen_subtitle_theme" format="reference|integer"/>
<attr name="lockscreen_hint_theme" format="reference|integer"/>
<attr name="lockscreen_next_theme" format="reference|integer"/>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="lockscreen_code">#ffffff</color>
<color name="lockscreen_white_selector">#66ffffff</color>
<color name="lockscreen_hint_color">#42000000</color>
<color name="lockscreen_warning_color">#f4511e</color>
<color name="lockscreen_success_color">#009688</color>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="lockscreen_button_size">70dp</dimen>
<dimen name="lockscreen_button_margin_vertical">25dp</dimen>
<dimen name="lockscreen_code_size">10dp</dimen>
<dimen name="lockscreen_code_margin">5dp</dimen>
</resources>

View file

@ -0,0 +1,17 @@
<resources>
<string name="lockscreen_cancel">Cancel</string>
<string name="lockscreen_use_pin">Use pin</string>
<string name="lockscreen_sign_in">Sign in</string>
<string name="lockscreen_next">Next</string>
<string name="lockscreen_forgot">Forgot?</string>
<string name="lockscreen_title">Input pin code or use biometric authentication</string>
<string name="lockscreen_fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
<string name="lockscreen_fingerprint_success">Fingerprint recognized</string>
<string name="lockscreen_fingerprint_description">Confirm fingerprint to continue</string>
<string name="lockscreen_fingerprint_hint">Touch sensor</string>
<string name="lockscreen_description_fingerprint_icon">Fingerprint icon</string>
<string name="lockscreen_confirm_pin">Confirm PIN</string>
<string name="lockscreen_description_logo">Logo</string>
</resources>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LockScreenStyle">
<item name="android:background">@drawable/lockscreen_background</item>
</style>
<style name="LockScreenButtonStyle" parent="Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="android:textColor">@android:color/white</item>
<item name="android:background">@drawable/lockscreen_touch_selector</item>
</style>
<style name="LockScreenFingerPrintButtonStyle">
<item name="android:src">@drawable/lockscreen_fingerprint</item>
<item name="android:padding">20dp</item>
</style>
<style name="LockScreenDeleteButtonStyle">
<item name="android:src">@drawable/lockscreen_delete</item>
<item name="android:padding">20dp</item>
</style>
<style name="CheckBox">
<item name="android:checkboxStyle">@style/LockScreenCodeStyle</item>
<item name="checkboxStyle">@style/LockScreenCodeStyle</item>
</style>
<style name="LockScreenCodeStyle">
<item name="android:button">@drawable/lockscreen_code_selector</item>
</style>
<style name="LockScreenNextTextStyle">
<item name="android:textColor">#9FFF</item>
<item name="android:textSize">18sp</item>
<item name="android:backgroundTint">#c66</item>
</style>
<style name="LockScreenHintTextStyle">
<item name="android:textColor">@android:color/white</item>
</style>
<style name="LockScreenTitleTextStyle">
<item name="android:textColor">@android:color/white</item>
<item name="android:textSize">18sp</item>
<item name="android:gravity">center</item>
</style>
</resources>

View file

@ -4,10 +4,13 @@
<!-- BottomSheet theming -->
<style name="Theme.Vector.BottomSheetDialog.Light" parent="Theme.MaterialComponents.Light.BottomSheetDialog">
<item name="colorPrimary">@color/element_accent_light</item>
<item name="colorOnPrimary">@color/palette_white</item>
<item name="colorSecondary">@color/palette_element_green</item>
<item name="colorOnSecondary">@color/palette_white</item>
<item name="colorSurface">@color/element_background_light</item>
<item name="colorOnSurface">@color/element_content_primary_light</item>
<item name="colorError">@color/element_alert_light</item>
<item name="colorOnError">@color/palette_white</item>
<!-- Default color for text View -->
<item name="android:textColorTertiary">@color/element_content_primary_light</item>
<item name="android:textColorLink">@color/element_link_light</item>
@ -15,10 +18,13 @@
<style name="Theme.Vector.BottomSheetDialog.Dark" parent="Theme.MaterialComponents.BottomSheetDialog">
<item name="colorPrimary">@color/element_accent_dark</item>
<item name="colorOnPrimary">@color/palette_white</item>
<item name="colorSecondary">@color/palette_element_green</item>
<item name="colorOnSecondary">@color/palette_white</item>
<item name="colorSurface">@color/element_background_dark</item>
<item name="colorOnSurface">@color/element_content_primary_dark</item>
<item name="colorError">@color/element_alert_dark</item>
<item name="colorOnError">@color/palette_white</item>
<!-- Default color for text View -->
<item name="android:textColorTertiary">@color/element_content_primary_dark</item>
<item name="android:textColorLink">@color/element_link_dark</item>
@ -59,4 +65,4 @@
<item name="android:textSize">12sp</item>
</style>
</resources>
</resources>

View file

@ -65,4 +65,4 @@
<item name="colorPrimary">?colorOnPrimary</item>
</style>
</resources>
</resources>

View file

@ -22,13 +22,13 @@
</style>
<style name="PinCodeDeleteButtonStyle">
<item name="android:src">@drawable/delete_lockscreen_pf</item>
<item name="android:src">@drawable/lockscreen_delete</item>
<item name="android:tint">?vctr_content_primary</item>
<item name="background">@drawable/bg_pin_key</item>
</style>
<style name="PinCodeFingerprintButtonStyle">
<item name="android:src">@drawable/fingerprint_lockscreen_pf</item>
<item name="android:src">@drawable/lockscreen_fingerprint</item>
<item name="android:tint">?vctr_content_primary</item>
<item name="background">@drawable/bg_pin_key</item>
</style>

View file

@ -111,14 +111,14 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
<item name="pf_title">@style/PinCodeTitleStyle</item>
<item name="pf_hint">@style/PinCodeHintStyle</item>
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
<item name="lockscreen_theme">@style/PinCodeScreenStyle</item>
<item name="lockscreen_key_button_theme">@style/PinCodeKeyButtonStyle</item>
<item name="lockscreen_title_theme">@style/PinCodeTitleStyle</item>
<item name="lockscreen_hint_theme">@style/PinCodeHintStyle</item>
<item name="lockscreen_code_view_theme">@style/PinCodeDotsViewStyle</item>
<item name="lockscreen_delete_button_theme">@style/PinCodeDeleteButtonStyle</item>
<item name="lockscreen_fingerprint_button_theme">@style/PinCodeFingerprintButtonStyle</item>
<item name="lockscreen_next_theme">@style/PinCodeNextButtonStyle</item>
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>
<item name="android:navigationBarColor">@color/android_navigation_bar_background_dark</item>

View file

@ -111,14 +111,14 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
<item name="pf_title">@style/PinCodeTitleStyle</item>
<item name="pf_hint">@style/PinCodeHintStyle</item>
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
<item name="lockscreen_theme">@style/PinCodeScreenStyle</item>
<item name="lockscreen_key_button_theme">@style/PinCodeKeyButtonStyle</item>
<item name="lockscreen_title_theme">@style/PinCodeTitleStyle</item>
<item name="lockscreen_hint_theme">@style/PinCodeHintStyle</item>
<item name="lockscreen_code_view_theme">@style/PinCodeDotsViewStyle</item>
<item name="lockscreen_delete_button_theme">@style/PinCodeDeleteButtonStyle</item>
<item name="lockscreen_fingerprint_button_theme">@style/PinCodeFingerprintButtonStyle</item>
<item name="lockscreen_next_theme">@style/PinCodeNextButtonStyle</item>
<!-- Use dark color, to have enough contrast with icons color. windowLightStatusBar is only available in API 23+ -->
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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<Event>,

View file

@ -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 &&

View file

@ -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)

View file

@ -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()`.
*/

View file

@ -21,5 +21,6 @@ data class LoginFlowResult(
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
val isOutdatedHomeserver: Boolean,
val isLogoutDevicesSupported: Boolean
)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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.
*/

View file

@ -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.

View file

@ -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 {

View file

@ -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<List<LiveLocationShareAggregatedSummary>>
/**
* 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<Optional<LiveLocationShareAggregatedSummary>>
}

View file

@ -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
}

View file

@ -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,

View file

@ -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,

View file

@ -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<PollAnswer>? = null
)
) {
fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED)
}

View file

@ -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

View file

@ -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.

View file

@ -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.
*/

View file

@ -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
}

View file

@ -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()
)
}

View file

@ -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) {

View file

@ -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
)
}
}

View file

@ -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)
}
}

View file

@ -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) }

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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<TimelineEventEntity>
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<TimelineEventEntity>().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<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {

View file

@ -271,7 +271,7 @@ private fun HashMap<String, RoomMemberContent?>.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)
}

View file

@ -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 {

View file

@ -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
)
}

View file

@ -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<LiveLocationShareAggregatedSummary, LiveLocationShareAggregatedSummaryEntity> {
fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
return LiveLocationShareAggregatedSummary(
userId = entity.userId,
isActive = entity.isActive,

View file

@ -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")

View file

@ -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)."
)
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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<ChunkEntity>? {
internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults<ChunkEntity> {
val query = where(realm, roomId)
if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)

View file

@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
realm: Realm,
roomId: String,
userId: String,
ignoredEventId: String
ignoredEventId: String,
): List<LiveLocationShareAggregatedSummaryEntity> {
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()
}
/**

Some files were not shown because too many files have changed in this diff Show more