Merge branch 'develop' into task/eric/when-arrow-alignment

This commit is contained in:
Benoit Marty 2022-06-07 23:03:36 +02:00 committed by GitHub
commit c290dd6c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 696 additions and 572 deletions

View File

@ -29,200 +29,6 @@ jobs:
steps:
- run: echo "Run those tests!" # no-op success
# Run Android Tests
integration-tests:
name: Matrix SDK - Running Integration Tests
needs: should-i-run
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
api-level: [ 28 ]
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 11
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.8
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
uses: michaelkaye/setup-matrix-synapse@v1.0.3
with:
uploadLogs: true
httpPort: 8080
disableRateLimiting: true
public_baseurl: "http://10.0.2.2:8080/"
# package: org.matrix.android.sdk.session
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
script: |
adb root
adb logcat -c
touch emulator-session.log
chmod 777 emulator-session.log
adb logcat >> emulator-session.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.session]
if: always()
id: get-comment-body-session
run: python3 ./tools/ci/render_test_output.py session ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
- name: Remove adb logcat
if: always()
run: pkill -9 adb
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.account] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
script: |
adb root
adb logcat -c
touch emulator-account.log
chmod 777 emulator-account.log
adb logcat >> emulator-account.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.account' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.account]
if: always()
id: get-comment-body-account
run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
- name: Remove adb logcat
if: always()
run: pkill -9 adb
# package: org.matrix.android.sdk.internal
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
script: |
adb root
adb logcat -c
touch emulator-internal.log
chmod 777 emulator-internal.log
adb logcat >> emulator-internal.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.internal' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.internal]
if: always()
id: get-comment-body-internal
run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
- name: Remove adb logcat
if: always()
run: pkill -9 adb
# package: org.matrix.android.sdk.ordering
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
script: |
adb root
adb logcat -c
touch emulator-ordering.log
chmod 777 emulator-ordering.log
adb logcat >> emulator-ordering.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.ordering' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.ordering]
if: always()
id: get-comment-body-ordering
run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
- name: Remove adb logcat
if: always()
run: pkill -9 adb
# package: class PermalinkParserTest
- name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-build: 7425822
script: |
adb root
adb logcat -c
touch emulator-permalink.log
chmod 777 emulator-permalink.log
adb logcat >> emulator-permalink.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class='org.matrix.android.sdk.PermalinkParserTest' matrix-sdk-android:connectedDebugAndroidTest
- name: Read Results [org.matrix.android.sdk.PermalinkParserTest]
if: always()
id: get-comment-body-permalink
run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
- name: Remove adb logcat
if: always()
run: pkill -9 adb
# package: class PermalinkParserTest
- name: Find Comment
if: always() && github.event_name == 'pull_request'
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Integration Tests Results
- name: Publish results to PR
if: always() && github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
### Matrix SDK
## Integration Tests Results:
- `[org.matrix.android.sdk.session]`<br>${{ steps.get-comment-body-session.outputs.session }}
- `[org.matrix.android.sdk.account]`<br>${{ steps.get-comment-body-account.outputs.account }}
- `[org.matrix.android.sdk.internal]`<br>${{ steps.get-comment-body-internal.outputs.internal }}
- `[org.matrix.android.sdk.ordering]`<br>${{ steps.get-comment-body-ordering.outputs.ordering }}
- `[org.matrix.android.sdk.PermalinkParserTest]`<br>${{ steps.get-comment-body-permalink.outputs.permalink }}
edit-mode: replace
- name: Upload Test Report Log
uses: actions/upload-artifact@v3
if: always()
with:
name: integrationtest-error-results
path: |
emulator-permalink.log
emulator-internal.log
emulator-ordering.log
emulator-account.log
emulator-session.log
ui-tests:
name: UI Tests (Synapse)
needs: should-i-run
@ -282,42 +88,13 @@ jobs:
emulator.log
failure_screenshots/
codecov-units:
name: Unit tests with code coverage
needs: should-i-run
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: ./gradlew allCodeCoverageReport $CI_GRADLE_ARG_PROPERTIES
- name: Upload Codecov data
uses: actions/upload-artifact@v3
if: always()
with:
name: codecov-xml
path: |
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
# Notify the channel about delayed failures
notify:
name: Notify matrix
runs-on: ubuntu-latest
needs:
- should-i-run
- integration-tests
- ui-tests
- codecov-units
if: always() && (needs.should-i-run.result == 'success' ) && ((needs.codecov-units.result != 'success' ) || (needs.ui-tests.result != 'success') || (needs.integration-tests.result != 'success'))
# No concurrency required, runs every time on a schedule.
steps:

View File

@ -1,81 +0,0 @@
name: Sonarqube nightly
on:
schedule:
- cron: '0 20 * * *'
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
jobs:
codecov-units:
name: Unit tests with code coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: ./gradlew allCodeCoverageReport $CI_GRADLE_ARG_PROPERTIES
- name: Upload Codecov data
uses: actions/upload-artifact@v3
if: always()
with:
name: codecov-xml
path: |
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
sonarqube:
name: Sonarqube upload
runs-on: ubuntu-latest
needs:
- codecov-units
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- uses: actions/download-artifact@v3
with:
name: codecov-xml # will restore to allCodeCoverageReport.xml by default; we restore to the same location in following tasks
- run: mkdir -p build/reports/jacoco/allCodeCoverageReport/
- run: mv allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/
- run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
env:
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
# Notify the channel about sonarqube failures
notify:
name: Notify matrix
runs-on: ubuntu-latest
needs:
- sonarqube
- codecov-units
if: always() && (needs.sonarqube.result != 'success' || needs.codecov-units.result != 'success')
steps:
- uses: michaelkaye/matrix-hookshot-action@v1.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
text_template: "Sonarqube run (on ${{ github.ref }}): {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
html_template: "Sonarqube run (on ${{ github.ref }}): {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"

View File

@ -12,73 +12,98 @@ env:
-Porg.gradle.parallel=false
jobs:
# Build Android Tests
build-android-tests:
name: Build Android Tests
runs-on: ubuntu-latest
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 11
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build Android Tests
run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
tests:
name: Runs all tests
runs-on: macos-latest # for the emulator
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run unit tests
run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES --stacktrace
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- uses: gradle/gradle-build-action@v2
- uses: actions/setup-python@v3
with:
python-version: 3.8
- uses: michaelkaye/setup-matrix-synapse@v1.0.3
with:
uploadLogs: true
httpPort: 8080
disableRateLimiting: true
public_baseurl: "http://10.0.2.2:8080/"
- name: Run all the codecoverage tests at once
id: tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 28
arch: x86
profile: Nexus 5X
force-avd-creation: false
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 --stacktrace $CI_GRADLE_ARG_PROPERTIES
- name: Run all the codecoverage tests at once (retry if emulator failed)
uses: reactivecircus/android-emulator-runner@v2
if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded.
with:
api-level: 28
arch: x86
profile: Nexus 5X
force-avd-creation: false
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 --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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
- name: Format unit test results
if: always()
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always() &&
github.event.sender.login != 'dependabot[bot]' &&
( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
with:
files: ./**/build/test-results/**/*.xml
# Notify the channel about runs against develop or main that have failures, as PRs should have caught these first.
notify:
runs-on: ubuntu-latest
needs:
- unit-tests
- build-android-tests
if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) && failure() }}
steps:
- uses: michaelkaye/matrix-hookshot-action@v0.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
text_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}{{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
html_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion }} {{name}} <font color='{{color conclusion }}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
# can't be run on macos due to containers.
# - name: Publish Unit Test Results
# uses: EnricoMi/publish-unit-test-result-action@v1
# if: always() &&
# github.event.sender.login != 'dependabot[bot]' &&
# ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
# with:
# files: ./**/build/test-results/**/*.xml
# Unneeded as part of the test suite above, kept around in case we want to re-enable them.
#
# # Build Android Tests
# build-android-tests:
# name: Build Android Tests
# runs-on: ubuntu-latest
# concurrency:
# group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }}
# cancel-in-progress: true
# steps:
# - uses: actions/checkout@v3
# - uses: actions/setup-java@v3
# with:
# distribution: 'adopt'
# java-version: 11
# - uses: actions/cache@v3
# with:
# path: |
# ~/.gradle/caches
# ~/.gradle/wrapper
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
# restore-keys: |
# ${{ runner.os }}-gradle-
# - name: Build Android Tests
# run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace

View File

@ -1,3 +1,11 @@
Changes in Element 1.4.19 (2022-06-07)
======================================
Bugfixes 🐛
----------
- Fix | performance regression on roomlist + proper display of space parents in explore rooms. ([#6233](https://github.com/vector-im/element-android/issues/6233))
Changes in Element v1.4.18 (2022-05-31)
=======================================

View File

@ -1,9 +1,9 @@
[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
[![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget)
[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=vector-im_element-android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=vector-im_element-android)
# Element Android

View File

@ -180,8 +180,8 @@ apply plugin: 'org.sonarqube'
sonarqube {
properties {
property "sonar.projectName", "Element-Android"
property "sonar.projectKey", "im.vector.app.android"
property "sonar.projectName", "element-android"
property "sonar.projectKey", "vector-im_element-android"
property "sonar.host.url", "https://sonarcloud.io"
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8"
@ -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/allCodeCoverageReport/allCodeCoverageReport.xml"
property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/theCodeCoverageReport/theCodeCoverageReport.xml"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
}
}

1
changelog.d/5285.wip Normal file
View File

@ -0,0 +1 @@
FTUE - Adds Sign Up tracking

1
changelog.d/6017.misc Normal file
View File

@ -0,0 +1 @@
Adds support for parsing homeserver versions without a patch number

1
changelog.d/6146.feature Normal file
View File

@ -0,0 +1 @@
Allow .well-known configuration to override key sharing mode

1
changelog.d/6169.sdk Normal file
View File

@ -0,0 +1 @@
Allows new passwords to be passed at the point of confirmation when resetting a password

1
changelog.d/6222.bugfix Normal file
View File

@ -0,0 +1 @@
Fix StackOverflowError while recording voice message

1
changelog.d/6232.bugfix Normal file
View File

@ -0,0 +1 @@
Text cropped: "Secure backup"

View File

@ -2,7 +2,10 @@ def excludes = [ ]
def initializeReport(report, projects, classExcludes) {
projects.each { project -> project.apply plugin: 'jacoco' }
report.executionData { fileTree(rootProject.rootDir.absolutePath).include("**/build/jacoco/*.exec") }
report.executionData { fileTree(rootProject.rootDir.absolutePath).include(
"**/build/outputs/unit_test_code_coverage/**/*.exec",
"**/build/outputs/code_coverage/**/coverage.ec"
) }
report.reports {
xml.enabled true
@ -18,11 +21,13 @@ 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
@ -43,13 +48,17 @@ def collectProjects(predicate) {
return subprojects.findAll { it.buildFile.isFile() && predicate(it) }
}
task allCodeCoverageReport(type: JacocoReport) {
task theCodeCoverageReport(type: JacocoReport) {
outputs.upToDateWhen { false }
rootProject.apply plugin: 'jacoco'
// to limit projects in a specific report, add
// def excludedProjects = [ ... ]
// def projects = collectProjects { !excludedProjects.contains(it.name) }
def projects = collectProjects { true }
dependsOn { projects*.test }
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
def projects = collectProjects { ['vector','matrix-sdk-android'].contains(it.name) }
dependsOn {
[':matrix-sdk-android:testDebugUnitTest'] +
[':vector:testGplayDebugUnitTest'] +
[':matrix-sdk-android:connectedDebugAndroidTest']
}
initializeReport(it, projects, excludes)
}

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

@ -74,6 +74,7 @@ android {
buildTypes {
debug {
testCoverageEnabled true
// 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

View File

@ -19,10 +19,14 @@ package org.matrix.android.sdk
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.test.shared.createTimberTestRule
interface InstrumentedTest {
@Rule
fun retryTestRule() = RetryTestRule(3)
@Rule
fun timberTestRule() = createTimberTestRule()

View File

@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -40,6 +41,7 @@ import java.util.UUID
@Suppress("SpellCheckingInspection")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class AttachmentEncryptionTest {
private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String {

View File

@ -22,6 +22,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -37,6 +38,7 @@ import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey"
@RunWith(AndroidJUnit4::class)
@Ignore
class CryptoStoreTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)

View File

@ -21,6 +21,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -30,6 +31,7 @@ import org.junit.runners.MethodSorters
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class ExportEncryptionTest {
@Test

View File

@ -21,6 +21,7 @@ import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -59,6 +60,7 @@ import kotlin.coroutines.resume
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@Ignore
class UnwedgingTest : InstrumentedTest {
private lateinit var messagesReceivedByBob: List<TimelineEvent>

View File

@ -25,6 +25,7 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -47,6 +48,7 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest
@Ignore
class XSigningTest : InstrumentedTest {
@Test

View File

@ -25,6 +25,7 @@ import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.Assert.assertNull
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -50,6 +51,7 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)

View File

@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -46,6 +47,7 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class WithHeldTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)

View File

@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -55,6 +56,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class KeysBackupTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)

View File

@ -52,6 +52,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class SASTest : InstrumentedTest {
@Test

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -41,6 +42,7 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@Ignore
class VerificationTest : InstrumentedTest {
data class ExpectedResult(

View File

@ -20,6 +20,7 @@ import androidx.test.filters.LargeTest
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.internal.assertEquals
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@ -38,6 +39,7 @@ import org.matrix.android.sdk.common.TestConstants
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class TimelineSimpleBackPaginationTest : InstrumentedTest {
@Test

View File

@ -22,6 +22,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@ -98,6 +99,7 @@ class SpaceCreationTest : InstrumentedTest {
}
@Test
@Ignore
fun testJoinSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))

View File

@ -65,16 +65,14 @@ interface LoginWizard {
* [resetPasswordMailConfirmed] is successfully called.
*
* @param email an email previously associated to the account the user wants the password to be reset.
* @param newPassword the desired new password
*/
suspend fun resetPassword(
email: String,
newPassword: String
)
suspend fun resetPassword(email: String)
/**
* 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
*/
suspend fun resetPasswordMailConfirmed()
suspend fun resetPasswordMailConfirmed(newPassword: String)
}

View File

@ -27,3 +27,8 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
* Append a new line and then the provided string.
*/
fun StringBuilder.appendNl(str: String) = append("\n").append(str)
/**
* Returns null if the string is empty.
*/
fun String.ensureNotEmpty() = ifEmpty { null }

View File

@ -226,12 +226,19 @@ interface RoomService {
): LiveData<PagedList<RoomSummary>>
/**
* TODO Doc.
* Get's a live paged list from a filter that can be dynamically updated.
*
* @param queryParams The filter to use
* @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
* @param sortOrder defines how to sort the results
* @param getFlattenParents When true, the list of known parents and grand parents summaries will be resolved.
* This can have significant impact on performance, better be used only on manageable list (filtered by displayName, ..).
*/
fun getFilteredPagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
getFlattenParents: Boolean = false,
): UpdatableLivePageResult
/**

View File

@ -103,7 +103,7 @@ internal class DefaultLoginWizard(
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
override suspend fun resetPassword(email: String, newPassword: String) {
override suspend fun resetPassword(email: String) {
val param = RegisterAddThreePidTask.Params(
RegisterThreePid.Email(email),
pendingSessionData.clientSecret,
@ -117,18 +117,16 @@ internal class DefaultLoginWizard(
authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result))
pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(result))
.also { pendingSessionStore.savePendingSessionData(it) }
}
override suspend fun resetPasswordMailConfirmed() {
val safeResetPasswordData = pendingSessionData.resetPasswordData
?: throw IllegalStateException("developer error, no reset password in progress")
override suspend fun resetPasswordMailConfirmed(newPassword: String) {
val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret,
safeResetPasswordData.addThreePidRegistrationResponse.sid,
safeResetPasswordData.newPassword
resetPasswordData.addThreePidRegistrationResponse.sid,
newPassword
)
executeRequest(null) {

View File

@ -24,6 +24,5 @@ import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistration
*/
@JsonClass(generateAdapter = true)
internal data class ResetPasswordData(
val newPassword: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
)

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.auth.version
import org.matrix.android.sdk.api.extensions.ensureNotEmpty
/**
* Values will take the form "rX.Y.Z".
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
@ -38,14 +40,14 @@ internal data class HomeServerVersion(
}
companion object {
internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
internal val pattern = Regex("""[r|v](\d+)\.(\d+)(?:\.(\d+))?""")
internal fun parse(value: String): HomeServerVersion? {
val result = pattern.matchEntire(value) ?: return null
return HomeServerVersion(
major = result.groupValues[1].toInt(),
minor = result.groupValues[2].toInt(),
patch = result.groupValues[3].toInt()
patch = result.groupValues.getOrNull(index = 3)?.ensureNotEmpty()?.toInt() ?: 0
)
}

View File

@ -139,9 +139,10 @@ internal class DefaultRoomService @Inject constructor(
override fun getFilteredPagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder
sortOrder: RoomSortOrder,
getFlattenParents: Boolean
): UpdatableLivePageResult {
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenedParents = true)
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents)
}
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {

View File

@ -0,0 +1,57 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.auth.version
import org.amshove.kluent.internal.assertEquals
import org.junit.Test
class HomeServerVersionTest {
@Test
fun `given a semantic version, when parsing, then converts to home server version`() {
val supportedVersions = listOf(
case("1.5", expected = aVersion(1, 5, 0)),
case("0.5.1", expected = aVersion(0, 5, 1)),
case("1.0.0", expected = aVersion(1, 0, 0)),
case("1.10.3", expected = aVersion(1, 10, 3)),
).withPrefixes("v", "r")
val unsupportedVersions = listOf(
case("v-1.5.1", expected = null),
case("1.4.", expected = null),
case("1.5.1.", expected = null),
case("r1", expected = null),
case("a", expected = null),
case("1a.2b.3c", expected = null),
case("r", expected = null),
)
(supportedVersions + unsupportedVersions).forEach { (input, expected) ->
val result = HomeServerVersion.parse(input)
assertEquals(expected, result, "Expected $input to be $expected but got $result")
}
}
}
private fun aVersion(major: Int, minor: Int, patch: Int) = HomeServerVersion(major, minor, patch)
private fun case(input: String, expected: HomeServerVersion?) = Case(input, expected)
private fun List<Case>.withPrefixes(vararg prefixes: String) = map { case ->
prefixes.map { prefix -> case.copy(input = "$prefix${case.input}") }
}.flatten()
private data class Case(val input: String, val expected: HomeServerVersion?)

View File

@ -244,6 +244,7 @@ android {
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
signingConfig signingConfigs.debug
testCoverageEnabled true
}
release {

View File

@ -0,0 +1,31 @@
/*
* 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.analytics.extensions
import im.vector.app.features.analytics.plan.Signup
import im.vector.app.features.onboarding.AuthenticationDescription
fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) {
AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password
AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple
AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook
AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub
AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab
AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google
AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO
AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other
}

View File

@ -19,6 +19,7 @@ package im.vector.app.features.crypto.keysrequest
enum class OutboundSessionKeySharingStrategy {
/**
* Keys will be sent for the first time when the first message is sent.
* This is handled by the Matrix SDK so there's no need to do it in Vector.
*/
WhenSendingEvent,

View File

@ -56,6 +56,7 @@ import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.navigation.Navigator
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.permalink.PermalinkHandler.Companion.MATRIX_TO_CUSTOM_SCHEME_URL_BASE
@ -91,7 +92,7 @@ import javax.inject.Inject
@Parcelize
data class HomeActivityArgs(
val clearNotification: Boolean,
val accountCreation: Boolean,
val authenticationDescription: AuthenticationDescription? = null,
val hasExistingSession: Boolean = false,
val inviteNotificationRoomId: String? = null
) : Parcelable
@ -612,13 +613,13 @@ class HomeActivity :
fun newIntent(
context: Context,
clearNotification: Boolean = false,
accountCreation: Boolean = false,
authenticationDescription: AuthenticationDescription? = null,
existingSession: Boolean = false,
inviteNotificationRoomId: String? = null
): Intent {
val args = HomeActivityArgs(
clearNotification = clearNotification,
accountCreation = accountCreation,
authenticationDescription = authenticationDescription,
hasExistingSession = existingSession,
inviteNotificationRoomId = inviteNotificationRoomId
)

View File

@ -28,17 +28,25 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType
import im.vector.app.features.analytics.plan.Signup
import im.vector.app.features.analytics.store.AnalyticsStore
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.raw.wellknown.ElementWellKnown
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import im.vector.app.features.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
@ -72,7 +80,8 @@ class HomeActivityViewModel @AssistedInject constructor(
private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val vectorPreferences: VectorPreferences
private val vectorPreferences: VectorPreferences,
private val analyticsTracker: AnalyticsTracker
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory
@ -84,7 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? {
val activity: HomeActivity = viewModelContext.activity()
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(Mavericks.KEY_ARG)
return args?.let { HomeActivityViewState(accountCreation = it.accountCreation) }
return args?.let { HomeActivityViewState(authenticationDescription = it.authenticationDescription) }
?: super.initialState(viewModelContext)
}
}
@ -113,9 +122,32 @@ class HomeActivityViewModel @AssistedInject constructor(
}
}
.launchIn(viewModelScope)
when (val recentAuthentication = initialState.authenticationDescription) {
is AuthenticationDescription.Register -> {
viewModelScope.launch {
analyticsStore.onUserGaveConsent {
analyticsTracker.capture(Signup(authenticationType = recentAuthentication.type.toAnalyticsType()))
}
}
}
AuthenticationDescription.Login -> {
// do nothing
}
null -> {
// do nothing
}
}
}
}
private suspend fun AnalyticsStore.onUserGaveConsent(action: () -> Unit) {
userConsentFlow
.takeWhile { !it }
.onCompletion { action() }
.collect()
}
private fun cleanupFiles() {
// Mitigation: delete all cached decrypted files each time the application is started.
activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
@ -134,9 +166,8 @@ class HomeActivityViewModel @AssistedInject constructor(
.onEach { info ->
val isVerified = info.getOrNull()?.isTrusted() ?: false
if (!isVerified && onceTrusted) {
viewModelScope.launch(Dispatchers.IO) {
val elementWellKnown = rawService.getElementWellknown(safeActiveSession.sessionParams)
sessionHasBeenUnverified(elementWellKnown)
rawService.withElementWellKnown(viewModelScope, safeActiveSession.sessionParams) {
sessionHasBeenUnverified(it)
}
}
onceTrusted = isVerified
@ -285,7 +316,7 @@ class HomeActivityViewModel @AssistedInject constructor(
val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
// In case of account creation, it is already done before
if (initialState.accountCreation) {
if (initialState.authenticationDescription is AuthenticationDescription.Register) {
if (isSecureBackupRequired) {
_viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
} else {

View File

@ -17,9 +17,10 @@
package im.vector.app.features.home
import com.airbnb.mvrx.MavericksState
import im.vector.app.features.onboarding.AuthenticationDescription
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
data class HomeActivityViewState(
val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle,
val accountCreation: Boolean = false
val authenticationDescription: AuthenticationDescription? = null
) : MavericksState

View File

@ -29,7 +29,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
@ -56,6 +55,8 @@ import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.location.LocationSharingServiceConnection
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault
import im.vector.app.features.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences
@ -76,6 +77,7 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
@ -118,6 +120,7 @@ class TimelineViewModel @AssistedInject constructor(
private val vectorDataStore: VectorDataStore,
private val stringProvider: StringProvider,
private val session: Session,
private val rawService: RawService,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler,
private val typingHelper: TypingHelper,
@ -196,8 +199,13 @@ class TimelineViewModel @AssistedInject constructor(
chatEffectManager.delegate = this
// Ensure to share the outbound session keys with all members
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.roomCryptoService().isEncrypted()) {
prepareForEncryption()
if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) {
prepareForEncryption()
}
}
}
// If the user had already accepted the invitation in the room list
@ -667,10 +675,13 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
// Ensure outbound session keys
if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.roomCryptoService().isEncrypted()) {
if (action.focused) {
// Should we add some rate limit here, or do it only once per model lifecycle?
prepareForEncryption()
if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) {
// Should we add some rate limit here, or do it only once per model lifecycle?
prepareForEncryption()
}
}
}
}

View File

@ -71,7 +71,7 @@ class RoomListSectionBuilderGroup(
},
{ qpm ->
val name = stringProvider.getString(R.string.bottom_action_rooms)
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(qpm)
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(qpm, getFlattenParents = true)
onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View File

@ -332,7 +332,7 @@ class RoomListSectionBuilderSpace(
},
{ queryParams ->
val name = stringProvider.getString(R.string.bottom_action_rooms)
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams)
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams, getFlattenParents = true)
onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View File

@ -207,7 +207,7 @@ class RoomSummaryItemFactory @Inject constructor(
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
val userId = roomSummary.directUserId
val spaceName = roomSummary.spaceParents?.firstOrNull()?.roomSummary?.name
val spaceName = roomSummary.flattenParents.lastOrNull()?.name
val canonicalAlias = roomSummary.canonicalAlias
return (userId ?: spaceName ?: canonicalAlias).orEmpty()

View File

@ -42,6 +42,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.login.terms.LoginTermsFragment
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.pin.UnlockedActivity
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
@ -218,10 +219,8 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
// change the screen name
analyticsScreenName = MobileScreen.ScreenName.Register
}
val intent = HomeActivity.newIntent(
this,
accountCreation = loginViewState.signMode == SignMode.SignUp
)
val authDescription = inferAuthDescription(loginViewState)
val intent = HomeActivity.newIntent(this, authenticationDescription = authDescription)
startActivity(intent)
finish()
return
@ -231,6 +230,13 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
views.loginLoading.isVisible = loginViewState.isLoading()
}
private fun inferAuthDescription(loginViewState: LoginViewState) = when (loginViewState.signMode) {
SignMode.Unknown -> null
SignMode.SignUp -> AuthenticationDescription.Register(type = AuthenticationDescription.AuthenticationType.Other)
SignMode.SignIn -> AuthenticationDescription.Login
SignMode.SignInWithMatrixId -> AuthenticationDescription.Login
}
private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) {
// Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -202,11 +203,11 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
providerId = provider?.id
)
?.let { openInCustomTab(it) }
}

View File

@ -25,6 +25,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import javax.inject.Inject
/**
@ -74,11 +75,11 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
providerId = provider?.id
)
?.let { openInCustomTab(it) }
}

View File

@ -413,7 +413,8 @@ class LoginViewModel @AssistedInject constructor(
copy(
asyncResetPassword = Uninitialized,
asyncResetMailConfirmed = Uninitialized,
resetPasswordEmail = null
resetPasswordEmail = null,
resetPasswordNewPassword = null
)
}
}
@ -488,7 +489,7 @@ class LoginViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPassword(action.email, action.newPassword)
safeLoginWizard.resetPassword(action.email)
} catch (failure: Throwable) {
setState {
copy(
@ -501,7 +502,8 @@ class LoginViewModel @AssistedInject constructor(
setState {
copy(
asyncResetPassword = Success(Unit),
resetPasswordEmail = action.email
resetPasswordEmail = action.email,
resetPasswordNewPassword = action.newPassword
)
}
@ -529,24 +531,35 @@ class LoginViewModel @AssistedInject constructor(
}
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPasswordMailConfirmed()
} catch (failure: Throwable) {
val state = awaitState()
if (state.resetPasswordNewPassword == null) {
setState {
copy(
asyncResetMailConfirmed = Fail(failure)
asyncResetPassword = Uninitialized,
asyncResetMailConfirmed = Fail(Throwable("Developer error - New password not set"))
)
}
return@launch
} else {
try {
safeLoginWizard.resetPasswordMailConfirmed(state.resetPasswordNewPassword)
} catch (failure: Throwable) {
setState {
copy(
asyncResetMailConfirmed = Fail(failure)
)
}
return@launch
}
setState {
copy(
asyncResetMailConfirmed = Success(Unit),
resetPasswordEmail = null,
resetPasswordNewPassword = null
)
}
_viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess)
}
setState {
copy(
asyncResetMailConfirmed = Success(Unit),
resetPasswordEmail = null
)
}
_viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess)
}
}
}

View File

@ -38,6 +38,8 @@ data class LoginViewState(
@PersistState
val resetPasswordEmail: String? = null,
@PersistState
val resetPasswordNewPassword: String? = null,
@PersistState
val homeServerUrlFromUser: String? = null,
// Can be modified after a Wellknown request

View File

@ -31,7 +31,7 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
LinearLayout(context, attrs, defStyle) {
fun interface InteractionListener {
fun onProviderSelected(id: String?)
fun onProviderSelected(provider: SsoIdentityProvider?)
}
enum class Mode {
@ -113,7 +113,7 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
button.text = getButtonTitle(identityProvider.name)
button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id)
button.setOnClickListener {
listener?.onProviderSelected(identityProvider.id)
listener?.onProviderSelected(identityProvider)
}
addView(button)
}
@ -160,7 +160,7 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
}
}
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (String?) -> Unit) {
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) {
this.mode = mode
this.ssoIdentityProviders = ssoProviders?.sorted()
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }

View File

@ -35,6 +35,7 @@ import im.vector.app.features.login.SocialLoginButtonsView
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject
@ -96,11 +97,11 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
providerId = provider?.id
)
?.let { openInCustomTab(it) }
}

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -123,11 +124,11 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
providerId = provider?.id
)
?.let { openInCustomTab(it) }
}

View File

@ -392,7 +392,8 @@ class LoginViewModel2 @AssistedInject constructor(
LoginAction2.ResetResetPassword -> {
setState {
copy(
resetPasswordEmail = null
resetPasswordEmail = null,
resetPasswordNewPassword = null
)
}
}
@ -443,7 +444,7 @@ class LoginViewModel2 @AssistedInject constructor(
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPassword(action.email, action.newPassword)
safeLoginWizard.resetPassword(action.email)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
@ -453,7 +454,8 @@ class LoginViewModel2 @AssistedInject constructor(
setState {
copy(
isLoading = false,
resetPasswordEmail = action.email
resetPasswordEmail = action.email,
resetPasswordNewPassword = action.newPassword
)
}
@ -472,7 +474,8 @@ class LoginViewModel2 @AssistedInject constructor(
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPasswordMailConfirmed()
val state = awaitState()
safeLoginWizard.resetPasswordMailConfirmed(state.resetPasswordNewPassword!!)
} catch (failure: Throwable) {
_viewEvents.post(LoginViewEvents2.Failure(failure))
setState { copy(isLoading = false) }
@ -481,7 +484,8 @@ class LoginViewModel2 @AssistedInject constructor(
setState {
copy(
isLoading = false,
resetPasswordEmail = null
resetPasswordEmail = null,
resetPasswordNewPassword = null
)
}

View File

@ -36,6 +36,8 @@ data class LoginViewState2(
@PersistState
val resetPasswordEmail: String? = null,
@PersistState
val resetPasswordNewPassword: String? = null,
@PersistState
val homeServerUrlFromUser: String? = null,
// Can be modified after a Wellknown request

View File

@ -0,0 +1,52 @@
/*
* 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.onboarding
import android.os.Parcelable
import im.vector.app.features.onboarding.AuthenticationDescription.AuthenticationType
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
sealed interface AuthenticationDescription : Parcelable {
@Parcelize
object Login : AuthenticationDescription
@Parcelize
data class Register(val type: AuthenticationType) : AuthenticationDescription
enum class AuthenticationType {
Password,
Apple,
Facebook,
GitHub,
GitLab,
Google,
SSO,
Other
}
}
fun SsoIdentityProvider?.toAuthenticationType() = when (this?.brand) {
SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google
SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub
SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple
SsoIdentityProvider.BRAND_FACEBOOK -> AuthenticationType.Facebook
SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab
SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO
null -> AuthenticationType.SSO
else -> AuthenticationType.SSO
}

View File

@ -276,7 +276,7 @@ class Login2Variant(
is LoginViewEvents2.OnLoginModeNotSupported ->
onLoginModeNotSupported(event.supportedTypes)
is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event)
is LoginViewEvents2.Finish -> terminate(true)
is LoginViewEvents2.Finish -> terminate()
is LoginViewEvents2.CancelRegistration -> handleCancelRegistration()
}
}
@ -296,14 +296,13 @@ class Login2Variant(
option = commonOption
)
} else {
terminate(false)
terminate()
}
}
private fun terminate(newAccount: Boolean) {
private fun terminate() {
val intent = HomeActivity.newIntent(
activity,
accountCreation = newAccount
activity
)
activity.startActivity(intent)
activity.finish()

View File

@ -54,6 +54,7 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
@ -127,7 +128,7 @@ class OnboardingViewModel @AssistedInject constructor(
val isRegistrationStarted: Boolean
get() = authenticationService.isRegistrationStarted()
private val loginWizard: LoginWizard?
private val loginWizard: LoginWizard
get() = authenticationService.getLoginWizard()
private var loginConfig: LoginConfig? = null
@ -245,21 +246,15 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleLoginWithToken(action: OnboardingAction.LoginWithToken) {
val safeLoginWizard = loginWizard
setState { copy(isLoading = true) }
if (safeLoginWizard == null) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
val result = safeLoginWizard.loginWithToken(action.loginToken)
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
currentJob = viewModelScope.launch {
try {
val result = safeLoginWizard.loginWithToken(action.loginToken)
onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
}
}
@ -289,7 +284,11 @@ class OnboardingViewModel @AssistedInject constructor(
// do nothing
}
else -> when (it) {
is RegistrationResult.Complete -> onSessionCreated(it.session, isAccountCreated = true)
is RegistrationResult.Complete -> onSessionCreated(
it.session,
authenticationDescription = awaitState().selectedAuthenticationState.description
?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other)
)
is RegistrationResult.NextStep -> onFlowResponse(it.flowResult, onNextRegistrationStepAction)
is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email))
is RegistrationResult.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause))
@ -319,6 +318,10 @@ class OnboardingViewModel @AssistedInject constructor(
private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
private fun handleRegisterWith(action: AuthenticateAction.Register) {
setState {
val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password)
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
}
reAuthHelper.data = action.password
handleRegisterAction(
RegisterAction.CreateAccount(
@ -368,7 +371,7 @@ class OnboardingViewModel @AssistedInject constructor(
setState {
copy(
isLoading = false,
resetPasswordEmail = null
resetState = ResetState()
)
}
}
@ -438,59 +441,52 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleResetPassword(action: OnboardingAction.ResetPassword) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPassword(action.email, action.newPassword)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
return@launch
}
setState {
copy(
isLoading = false,
resetPasswordEmail = action.email
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
}
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
onSuccess = {
setState {
copy(
isLoading = false,
resetState = ResetState(email = action.email, newPassword = action.newPassword)
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
},
onFailure = {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(it))
}
)
}
}
private fun handleResetPasswordMailConfirmed() {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = false) }
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPasswordMailConfirmed()
} catch (failure: Throwable) {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
val resetState = awaitState().resetState
when (val newPassword = resetState.newPassword) {
null -> {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
return@launch
_viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set")))
}
setState {
copy(
isLoading = false,
resetPasswordEmail = null
else -> {
runCatching { loginWizard.resetPasswordMailConfirmed(newPassword) }.fold(
onSuccess = {
setState {
copy(
isLoading = false,
resetState = ResetState()
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess)
},
onFailure = {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(it))
}
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess)
}
}
}
@ -499,7 +495,7 @@ class OnboardingViewModel @AssistedInject constructor(
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
onSuccess = { onSessionCreated(it, isAccountCreated = false) },
onSuccess = { onSessionCreated(it, authenticationDescription = AuthenticationDescription.Login) },
onFailure = {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(it))
@ -510,25 +506,19 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleLogin(action: AuthenticateAction.Login) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration")))
} else {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
val result = safeLoginWizard.login(
action.username,
action.password,
action.initialDeviceName
)
reAuthHelper.data = action.password
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
try {
val result = safeLoginWizard.login(
action.username,
action.password,
action.initialDeviceName
)
reAuthHelper.data = action.password
onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
}
}
@ -553,7 +543,7 @@ class OnboardingViewModel @AssistedInject constructor(
internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction)
}
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
private suspend fun onSessionCreated(session: Session, authenticationDescription: AuthenticationDescription) {
val state = awaitState()
state.useCase?.let { useCase ->
session.vectorStore(applicationContext).setUseCase(useCase)
@ -564,15 +554,15 @@ class OnboardingViewModel @AssistedInject constructor(
authenticationService.reset()
session.configureAndStart(applicationContext)
when (isAccountCreated) {
true -> {
when (authenticationDescription) {
is AuthenticationDescription.Register -> {
val personalizationState = createPersonalizationState(session, state)
setState {
copy(isLoading = false, personalizationState = personalizationState)
}
_viewEvents.post(OnboardingViewEvents.OnAccountCreated)
}
false -> {
AuthenticationDescription.Login -> {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
}
@ -603,7 +593,7 @@ class OnboardingViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch {
try {
val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
onSessionCreated(result, isAccountCreated = false)
onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
}
@ -745,8 +735,12 @@ class OnboardingViewModel @AssistedInject constructor(
return loginConfig?.homeServerUrl
}
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? {
return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId)
fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? {
setState {
val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType())
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
}
return authenticationService.getSsoUrl(redirectUrl, deviceId, provider?.id)
}
fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {

View File

@ -39,7 +39,7 @@ data class OnboardingViewState(
@PersistState
val signMode: SignMode = SignMode.Unknown,
@PersistState
val resetPasswordEmail: String? = null,
val resetState: ResetState = ResetState(),
// For SSO session recovery
@PersistState
@ -51,6 +51,9 @@ data class OnboardingViewState(
@PersistState
val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(),
@PersistState
val selectedAuthenticationState: SelectedAuthenticationState = SelectedAuthenticationState(),
@PersistState
val personalizationState: PersonalizationState = PersonalizationState()
) : MavericksState
@ -80,3 +83,14 @@ data class PersonalizationState(
fun supportsPersonalization() = supportsChangingDisplayName || supportsChangingProfilePicture
}
@Parcelize
data class ResetState(
val email: String? = null,
val newPassword: String? = null,
) : Parcelable
@Parcelize
data class SelectedAuthenticationState(
val description: AuthenticationDescription? = null,
) : Parcelable

View File

@ -153,7 +153,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
final override fun invalidate() = withState(viewModel) { state ->
// True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
isResetPasswordStarted = state.resetState.email.isNullOrBlank().not()
updateWithState(state)
}

View File

@ -90,10 +90,10 @@ abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthF
withState(viewModel) { state ->
if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders().isNullOrEmpty()) {
// in this case we can prefetch (not other cases for privacy concerns)
viewModel.getSsoUrl(
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
provider = null
)
?.let { prefetchUrl(it) }
}

View File

@ -131,10 +131,10 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.getSsoUrl(
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,
providerId = id
provider = id
)?.let { openInCustomTab(it) }
}
}

View File

@ -164,11 +164,11 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.getSsoUrl(
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,
providerId = id
provider = provider
)?.let { openInCustomTab(it) }
}
}

View File

@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
@ -216,11 +217,11 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
viewModel.getSsoUrl(
override fun onProviderSelected(provider: SsoIdentityProvider?) {
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
provider = provider
)
?.let { openInCustomTab(it) }
}

View File

@ -44,7 +44,7 @@ class FtueAuthResetPasswordMailConfirmationFragment @Inject constructor() : Abst
}
private fun setupUi(state: OnboardingViewState) {
views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetPasswordEmail)
views.resetPasswordMailConfirmationNotice.text = getString(R.string.login_reset_password_mail_confirmation_notice, state.resetState.email)
}
private fun submit() {

View File

@ -34,6 +34,7 @@ import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.ssoIdentityProviders
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import javax.inject.Inject
/**
@ -81,11 +82,11 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders()?.sorted()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
viewModel.getSsoUrl(
override fun onProviderSelected(provider: SsoIdentityProvider?) {
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
provider = provider
)
?.let { openInCustomTab(it) }
}
@ -123,10 +124,10 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF
private fun submit() = withState(viewModel) { state ->
if (state.selectedHomeserver.preferredLoginMode is LoginMode.Sso) {
viewModel.getSsoUrl(
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
provider = null
)
?.let { openInCustomTab(it) }
} else {

View File

@ -216,7 +216,7 @@ class FtueAuthVariant(
is OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn()
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
OnboardingViewEvents.OnTakeMeHome -> navigateToHome()
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
OnboardingViewEvents.OnPersonalizationComplete -> onPersonalizationComplete()
OnboardingViewEvents.OnBack -> activity.popBackstack()
@ -467,7 +467,7 @@ class FtueAuthVariant(
}
private fun onAccountSignedIn() {
navigateToHome(createdAccount = false)
navigateToHome()
}
private fun onAccountCreated() {
@ -479,10 +479,12 @@ class FtueAuthVariant(
)
}
private fun navigateToHome(createdAccount: Boolean) {
val intent = HomeActivity.newIntent(activity, accountCreation = createdAccount)
activity.startActivity(intent)
activity.finish()
private fun navigateToHome() {
withState(onboardingViewModel) {
val intent = HomeActivity.newIntent(activity, authenticationDescription = it.selectedAuthenticationState.description)
activity.startActivity(intent)
activity.finish()
}
}
private fun onChooseDisplayName() {

View File

@ -65,7 +65,14 @@ data class E2EWellKnownConfig(
* clients should fallback to the default value of: ["key", "passphrase"].
*/
@Json(name = "secure_backup_setup_methods")
val secureBackupSetupMethods: List<String>? = null
val secureBackupSetupMethods: List<String>? = null,
/**
* Configuration for sharing keys strategy which should be used instead of [im.vector.app.BuildConfig.outboundSessionKeySharingStrategy].
* One of on_room_opening, on_typing or disabled.
*/
@Json(name = "outbound_keys_pre_sharing_mode")
val outboundsKeyPreSharingMode: String? = null,
)
@JsonClass(generateAdapter = true)

View File

@ -16,6 +16,11 @@
package im.vector.app.features.raw.wellknown
import im.vector.app.BuildConfig
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -30,6 +35,25 @@ suspend fun RawService.getElementWellknown(sessionParams: SessionParams): Elemen
fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true
fun ElementWellKnown?.getOutboundSessionKeySharingStrategyOrDefault(): OutboundSessionKeySharingStrategy {
return when (this?.elementE2E?.outboundsKeyPreSharingMode) {
"on_room_opening" -> OutboundSessionKeySharingStrategy.WhenEnteringRoom
"on_typing" -> OutboundSessionKeySharingStrategy.WhenTyping
"disabled" -> OutboundSessionKeySharingStrategy.WhenSendingEvent
else -> BuildConfig.outboundSessionKeySharingStrategy
}
}
fun RawService.withElementWellKnown(
coroutineScope: CoroutineScope,
sessionParams: SessionParams,
block: ((ElementWellKnown?) -> Unit)
) = with(coroutineScope) {
launch(Dispatchers.IO) {
block(getElementWellknown(sessionParams))
}
}
fun ElementWellKnown.isSecureBackupRequired() = elementE2E?.secureBackupRequired
?: riotE2E?.secureBackupRequired
?: false

View File

@ -151,12 +151,14 @@ class AudioWaveformView @JvmOverloads constructor(
private fun handleNewFftList(fftList: List<FFT>) {
val maxVisibleBarCount = getMaxVisibleBarCount()
fftList.forEach { fft ->
rawFftList.add(fft)
val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight)
visibleBarHeights.add(FFT(barHeight, fft.color))
if (visibleBarHeights.size > maxVisibleBarCount) {
visibleBarHeights = visibleBarHeights.subList(visibleBarHeights.size - maxVisibleBarCount, visibleBarHeights.size)
visibleBarHeights = visibleBarHeights.takeLast(maxVisibleBarCount).toMutableList()
}
}
}

View File

@ -37,6 +37,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:textColor="?vctr_content_primary"
android:textStyle="bold"

View File

@ -31,6 +31,7 @@ import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeDirectLoginUseCase
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
import im.vector.app.test.fakes.FakeHomeServerHistoryService
import im.vector.app.test.fakes.FakeLoginWizard
import im.vector.app.test.fakes.FakeRegisterActionHandler
import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession
@ -67,6 +68,8 @@ private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
private const val AN_EMAIL = "hello@example.com"
private const val A_PASSWORD = "a-password"
class OnboardingViewModelTest {
@ -85,6 +88,7 @@ class OnboardingViewModelTest {
private val fakeHomeServerConnectionConfigFactory = FakeHomeServerConnectionConfigFactory()
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
private val fakeLoginWizard = FakeLoginWizard()
private var initialState = OnboardingViewState()
private lateinit var viewModel: OnboardingViewModel
@ -466,6 +470,43 @@ class OnboardingViewModelTest {
.finish()
}
@Test
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest {
val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
viewModel.handle(OnboardingAction.ResetPassword(email = AN_EMAIL, newPassword = A_PASSWORD))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState(AN_EMAIL, A_PASSWORD)) }
)
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
.finish()
}
@Test
fun `given can successfully confirm reset password, when confirm reset password, then emits reset success`() = runTest {
viewModelWith(initialState.copy(resetState = ResetState(AN_EMAIL, A_PASSWORD)))
val test = viewModel.test()
fakeLoginWizard.givenConfirmResetPasswordSuccess(A_PASSWORD)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
viewModel.handle(OnboardingAction.ResetPasswordMailConfirmed)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState()) }
)
.assertEvents(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess)
.finish()
}
private fun viewModelWith(state: OnboardingViewState) {
OnboardingViewModel(
state,

View File

@ -23,6 +23,7 @@ import io.mockk.mockk
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@ -36,6 +37,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
every { isRegistrationStarted() } returns started
}
fun givenLoginWizard(loginWizard: LoginWizard) {
every { getLoginWizard() } returns loginWizard
}
fun givenLoginFlow(config: HomeServerConnectionConfig, result: LoginFlowResult) {
coEvery { getLoginFlow(config) } returns result
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import io.mockk.coJustRun
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.login.LoginWizard
class FakeLoginWizard : LoginWizard by mockk() {
fun givenResetPasswordSuccess(email: String) {
coJustRun { resetPassword(email) }
}
fun givenConfirmResetPasswordSuccess(password: String) {
coJustRun { resetPasswordMailConfirmed(password) }
}
}