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: steps:
- run: echo "Run those tests!" # no-op success - 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: ui-tests:
name: UI Tests (Synapse) name: UI Tests (Synapse)
needs: should-i-run needs: should-i-run
@ -282,42 +88,13 @@ jobs:
emulator.log emulator.log
failure_screenshots/ 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 the channel about delayed failures
notify: notify:
name: Notify matrix name: Notify matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- should-i-run - should-i-run
- integration-tests
- ui-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')) 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. # No concurrency required, runs every time on a schedule.
steps: 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 -Porg.gradle.parallel=false
jobs: jobs:
# Build Android Tests tests:
build-android-tests: name: Runs all tests
name: Build Android Tests runs-on: macos-latest # for the emulator
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
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: 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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/cache@v3
with: with:
path: | fetch-depth: 0
~/.gradle/caches - uses: actions/setup-java@v3
~/.gradle/wrapper with:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} distribution: 'adopt'
restore-keys: | java-version: '11'
${{ runner.os }}-gradle- - uses: gradle/gradle-build-action@v2
- name: Run unit tests - uses: actions/setup-python@v3
run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES --stacktrace 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 - name: Format unit test results
if: always() if: always()
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml 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. # can't be run on macos due to containers.
notify: # - name: Publish Unit Test Results
runs-on: ubuntu-latest # uses: EnricoMi/publish-unit-test-result-action@v1
needs: # if: always() &&
- unit-tests # github.event.sender.login != 'dependabot[bot]' &&
- build-android-tests # ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) && failure() }} # with:
steps: # files: ./**/build/test-results/**/*.xml
- uses: michaelkaye/matrix-hookshot-action@v0.3.0
with: # Unneeded as part of the test suite above, kept around in case we want to re-enable them.
github_token: ${{ secrets.GITHUB_TOKEN }} #
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }} # # Build Android Tests
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }} # build-android-tests:
text_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}{{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" # name: Build Android Tests
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}}" # 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) 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) [![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) [![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) [![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) [![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=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.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=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.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 # Element Android

View file

@ -180,8 +180,8 @@ apply plugin: 'org.sonarqube'
sonarqube { sonarqube {
properties { properties {
property "sonar.projectName", "Element-Android" property "sonar.projectName", "element-android"
property "sonar.projectKey", "im.vector.app.android" property "sonar.projectKey", "vector-im_element-android"
property "sonar.host.url", "https://sonarcloud.io" property "sonar.host.url", "https://sonarcloud.io"
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8" property "sonar.sourceEncoding", "UTF-8"
@ -191,7 +191,7 @@ sonarqube {
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues" property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization" property "sonar.organization", "new_vector_ltd_organization"
property "sonar.java.coveragePlugin", "jacoco" 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" 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) { def initializeReport(report, projects, classExcludes) {
projects.each { project -> project.apply plugin: 'jacoco' } 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 { report.reports {
xml.enabled true xml.enabled true
@ -18,11 +21,13 @@ def initializeReport(report, projects, classExcludes) {
switch (project) { switch (project) {
case { project.plugins.hasPlugin("com.android.application") }: case { project.plugins.hasPlugin("com.android.application") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/gplayDebug") 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/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java") androidSourceDirs.add("${project.projectDir}/src/main/java")
break break
case { project.plugins.hasPlugin("com.android.library") }: case { project.plugins.hasPlugin("com.android.library") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug") 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/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java") androidSourceDirs.add("${project.projectDir}/src/main/java")
break break
@ -43,13 +48,17 @@ def collectProjects(predicate) {
return subprojects.findAll { it.buildFile.isFile() && predicate(it) } return subprojects.findAll { it.buildFile.isFile() && predicate(it) }
} }
task allCodeCoverageReport(type: JacocoReport) { task theCodeCoverageReport(type: JacocoReport) {
outputs.upToDateWhen { false } outputs.upToDateWhen { false }
rootProject.apply plugin: 'jacoco' rootProject.apply plugin: 'jacoco'
// to limit projects in a specific report, add tasks.withType(Test) {
// def excludedProjects = [ ... ] jacoco.includeNoLocationClasses = true
// def projects = collectProjects { !excludedProjects.contains(it.name) } }
def projects = collectProjects { true } def projects = collectProjects { ['vector','matrix-sdk-android'].contains(it.name) }
dependsOn { projects*.test } dependsOn {
[':matrix-sdk-android:testDebugUnitTest'] +
[':vector:testGplayDebugUnitTest'] +
[':matrix-sdk-android:connectedDebugAndroidTest']
}
initializeReport(it, projects, excludes) 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 { buildTypes {
debug { debug {
testCoverageEnabled true
// Set to true to log privacy or sensible data, such as token // Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
// Set to BODY instead of NONE to enable logging // 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 android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import org.junit.Rule import org.junit.Rule
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.test.shared.createTimberTestRule import org.matrix.android.sdk.test.shared.createTimberTestRule
interface InstrumentedTest { interface InstrumentedTest {
@Rule
fun retryTestRule() = RetryTestRule(3)
@Rule @Rule
fun timberTestRule() = createTimberTestRule() 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.assertEquals
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
@ -40,6 +41,7 @@ import java.util.UUID
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class AttachmentEncryptionTest { class AttachmentEncryptionTest {
private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String { 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.assertNotEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -37,6 +38,7 @@ import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey" private const val DUMMY_DEVICE_KEY = "DeviceKey"
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Ignore
class CryptoStoreTest : InstrumentedTest { class CryptoStoreTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @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.assertTrue
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
@ -30,6 +31,7 @@ import org.junit.runners.MethodSorters
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class ExportEncryptionTest { class ExportEncryptionTest {
@Test @Test

View file

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

View file

@ -25,6 +25,7 @@ import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -50,6 +51,7 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class KeyShareTests : InstrumentedTest { class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @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 androidx.test.filters.LargeTest
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -46,6 +47,7 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class WithHeldTests : InstrumentedTest { class WithHeldTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @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.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -55,6 +56,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class KeysBackupTest : InstrumentedTest { class KeysBackupTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @get:Rule val rule = RetryTestRule(3)

View file

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

View file

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

View file

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

View file

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

View file

@ -65,16 +65,14 @@ interface LoginWizard {
* [resetPasswordMailConfirmed] is successfully called. * [resetPasswordMailConfirmed] is successfully called.
* *
* @param email an email previously associated to the account the user wants the password to be reset. * @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( suspend fun resetPassword(email: String)
email: String,
newPassword: String
)
/** /**
* Confirm the new password, once the user has checked their email * Confirm the new password, once the user has checked their email
* When this method succeed, tha account password will be effectively modified. * 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. * Append a new line and then the provided string.
*/ */
fun StringBuilder.appendNl(str: String) = append("\n").append(str) 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>> ): 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( fun getFilteredPagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig, pagedListConfig: PagedList.Config = defaultPagedListConfig,
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
getFlattenParents: Boolean = false,
): UpdatableLivePageResult ): UpdatableLivePageResult
/** /**

View file

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

View file

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

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.auth.version package org.matrix.android.sdk.internal.auth.version
import org.matrix.android.sdk.api.extensions.ensureNotEmpty
/** /**
* Values will take the form "rX.Y.Z". * Values will take the form "rX.Y.Z".
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
@ -38,14 +40,14 @@ internal data class HomeServerVersion(
} }
companion object { 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? { internal fun parse(value: String): HomeServerVersion? {
val result = pattern.matchEntire(value) ?: return null val result = pattern.matchEntire(value) ?: return null
return HomeServerVersion( return HomeServerVersion(
major = result.groupValues[1].toInt(), major = result.groupValues[1].toInt(),
minor = result.groupValues[2].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( override fun getFilteredPagedRoomSummariesLive(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config, pagedListConfig: PagedList.Config,
sortOrder: RoomSortOrder sortOrder: RoomSortOrder,
getFlattenParents: Boolean
): UpdatableLivePageResult { ): UpdatableLivePageResult {
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenedParents = true) return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents)
} }
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> { 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" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
testCoverageEnabled true
} }
release { 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 { enum class OutboundSessionKeySharingStrategy {
/** /**
* Keys will be sent for the first time when the first message is sent. * 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, 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.matrixto.OriginOfMatrixTo
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.app.features.notifications.NotificationDrawerManager 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.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.permalink.PermalinkHandler.Companion.MATRIX_TO_CUSTOM_SCHEME_URL_BASE import im.vector.app.features.permalink.PermalinkHandler.Companion.MATRIX_TO_CUSTOM_SCHEME_URL_BASE
@ -91,7 +92,7 @@ import javax.inject.Inject
@Parcelize @Parcelize
data class HomeActivityArgs( data class HomeActivityArgs(
val clearNotification: Boolean, val clearNotification: Boolean,
val accountCreation: Boolean, val authenticationDescription: AuthenticationDescription? = null,
val hasExistingSession: Boolean = false, val hasExistingSession: Boolean = false,
val inviteNotificationRoomId: String? = null val inviteNotificationRoomId: String? = null
) : Parcelable ) : Parcelable
@ -612,13 +613,13 @@ class HomeActivity :
fun newIntent( fun newIntent(
context: Context, context: Context,
clearNotification: Boolean = false, clearNotification: Boolean = false,
accountCreation: Boolean = false, authenticationDescription: AuthenticationDescription? = null,
existingSession: Boolean = false, existingSession: Boolean = false,
inviteNotificationRoomId: String? = null inviteNotificationRoomId: String? = null
): Intent { ): Intent {
val args = HomeActivityArgs( val args = HomeActivityArgs(
clearNotification = clearNotification, clearNotification = clearNotification,
accountCreation = accountCreation, authenticationDescription = authenticationDescription,
hasExistingSession = existingSession, hasExistingSession = existingSession,
inviteNotificationRoomId = inviteNotificationRoomId 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.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel 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.analytics.store.AnalyticsStore
import im.vector.app.features.login.ReAuthHelper 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.ElementWellKnown
import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isSecureBackupRequired 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.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
@ -72,7 +80,8 @@ class HomeActivityViewModel @AssistedInject constructor(
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
private val analyticsTracker: AnalyticsTracker
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -84,7 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? { override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? {
val activity: HomeActivity = viewModelContext.activity() val activity: HomeActivity = viewModelContext.activity()
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(Mavericks.KEY_ARG) 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) ?: super.initialState(viewModelContext)
} }
} }
@ -113,9 +122,32 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
.launchIn(viewModelScope) .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() { private fun cleanupFiles() {
// Mitigation: delete all cached decrypted files each time the application is started. // Mitigation: delete all cached decrypted files each time the application is started.
activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache() activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
@ -134,9 +166,8 @@ class HomeActivityViewModel @AssistedInject constructor(
.onEach { info -> .onEach { info ->
val isVerified = info.getOrNull()?.isTrusted() ?: false val isVerified = info.getOrNull()?.isTrusted() ?: false
if (!isVerified && onceTrusted) { if (!isVerified && onceTrusted) {
viewModelScope.launch(Dispatchers.IO) { rawService.withElementWellKnown(viewModelScope, safeActiveSession.sessionParams) {
val elementWellKnown = rawService.getElementWellknown(safeActiveSession.sessionParams) sessionHasBeenUnverified(it)
sessionHasBeenUnverified(elementWellKnown)
} }
} }
onceTrusted = isVerified onceTrusted = isVerified
@ -285,7 +316,7 @@ class HomeActivityViewModel @AssistedInject constructor(
val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
// In case of account creation, it is already done before // In case of account creation, it is already done before
if (initialState.accountCreation) { if (initialState.authenticationDescription is AuthenticationDescription.Register) {
if (isSecureBackupRequired) { if (isSecureBackupRequired) {
_viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
} else { } else {

View file

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

View file

@ -29,7 +29,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler import im.vector.app.AppStateHandler
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory 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.location.LocationSharingServiceConnection
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory 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.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences 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.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue 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.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -118,6 +120,7 @@ class TimelineViewModel @AssistedInject constructor(
private val vectorDataStore: VectorDataStore, private val vectorDataStore: VectorDataStore,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val session: Session, private val session: Session,
private val rawService: RawService,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler, private val stickerPickerActionHandler: StickerPickerActionHandler,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
@ -196,8 +199,13 @@ class TimelineViewModel @AssistedInject constructor(
chatEffectManager.delegate = this chatEffectManager.delegate = this
// Ensure to share the outbound session keys with all members // Ensure to share the outbound session keys with all members
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
prepareForEncryption() 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 // 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) { private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
// Ensure outbound session keys // Ensure outbound session keys
if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
if (action.focused) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
// Should we add some rate limit here, or do it only once per model lifecycle? val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
prepareForEncryption() 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 -> { qpm ->
val name = stringProvider.getString(R.string.bottom_action_rooms) 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) onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View file

@ -332,7 +332,7 @@ class RoomListSectionBuilderSpace(
}, },
{ queryParams -> { queryParams ->
val name = stringProvider.getString(R.string.bottom_action_rooms) 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) onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()

View file

@ -207,7 +207,7 @@ class RoomSummaryItemFactory @Inject constructor(
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String { private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
val userId = roomSummary.directUserId val userId = roomSummary.directUserId
val spaceName = roomSummary.spaceParents?.firstOrNull()?.roomSummary?.name val spaceName = roomSummary.flattenParents.lastOrNull()?.name
val canonicalAlias = roomSummary.canonicalAlias val canonicalAlias = roomSummary.canonicalAlias
return (userId ?: spaceName ?: canonicalAlias).orEmpty() 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.home.HomeActivity
import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragment
import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.UnlockedActivity
import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
@ -218,10 +219,8 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
// change the screen name // change the screen name
analyticsScreenName = MobileScreen.ScreenName.Register analyticsScreenName = MobileScreen.ScreenName.Register
} }
val intent = HomeActivity.newIntent( val authDescription = inferAuthDescription(loginViewState)
this, val intent = HomeActivity.newIntent(this, authenticationDescription = authDescription)
accountCreation = loginViewState.signMode == SignMode.SignUp
)
startActivity(intent) startActivity(intent)
finish() finish()
return return
@ -231,6 +230,13 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
views.loginLoading.isVisible = loginViewState.isLoading() 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) { private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) {
// Pop the backstack // Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) 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.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach 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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -202,11 +203,11 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
views.loginSocialLoginContainer.isVisible = true views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted() views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = provider?.id
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }

View file

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

View file

@ -413,7 +413,8 @@ class LoginViewModel @AssistedInject constructor(
copy( copy(
asyncResetPassword = Uninitialized, asyncResetPassword = Uninitialized,
asyncResetMailConfirmed = Uninitialized, asyncResetMailConfirmed = Uninitialized,
resetPasswordEmail = null resetPasswordEmail = null,
resetPasswordNewPassword = null
) )
} }
} }
@ -488,7 +489,7 @@ class LoginViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
safeLoginWizard.resetPassword(action.email, action.newPassword) safeLoginWizard.resetPassword(action.email)
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { setState {
copy( copy(
@ -501,7 +502,8 @@ class LoginViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
asyncResetPassword = Success(Unit), asyncResetPassword = Success(Unit),
resetPasswordEmail = action.email resetPasswordEmail = action.email,
resetPasswordNewPassword = action.newPassword
) )
} }
@ -529,24 +531,35 @@ class LoginViewModel @AssistedInject constructor(
} }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { val state = awaitState()
safeLoginWizard.resetPasswordMailConfirmed()
} catch (failure: Throwable) { if (state.resetPasswordNewPassword == null) {
setState { setState {
copy( 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 @PersistState
val resetPasswordEmail: String? = null, val resetPasswordEmail: String? = null,
@PersistState @PersistState
val resetPasswordNewPassword: String? = null,
@PersistState
val homeServerUrlFromUser: String? = null, val homeServerUrlFromUser: String? = null,
// Can be modified after a Wellknown request // Can be modified after a Wellknown request

View file

@ -31,7 +31,7 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
LinearLayout(context, attrs, defStyle) { LinearLayout(context, attrs, defStyle) {
fun interface InteractionListener { fun interface InteractionListener {
fun onProviderSelected(id: String?) fun onProviderSelected(provider: SsoIdentityProvider?)
} }
enum class Mode { enum class Mode {
@ -113,7 +113,7 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
button.text = getButtonTitle(identityProvider.name) button.text = getButtonTitle(identityProvider.name)
button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id) button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id)
button.setOnClickListener { button.setOnClickListener {
listener?.onProviderSelected(identityProvider.id) listener?.onProviderSelected(identityProvider)
} }
addView(button) 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.mode = mode
this.ssoIdentityProviders = ssoProviders?.sorted() this.ssoIdentityProviders = ssoProviders?.sorted()
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) } 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.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import reactivecircus.flowbinding.android.widget.textChanges import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject import javax.inject.Inject
@ -96,11 +97,11 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm
views.loginSocialLoginContainer.isVisible = true views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted() views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(provider: SsoIdentityProvider?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = provider?.id
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }

View file

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

View file

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

View file

@ -36,6 +36,8 @@ data class LoginViewState2(
@PersistState @PersistState
val resetPasswordEmail: String? = null, val resetPasswordEmail: String? = null,
@PersistState @PersistState
val resetPasswordNewPassword: String? = null,
@PersistState
val homeServerUrlFromUser: String? = null, val homeServerUrlFromUser: String? = null,
// Can be modified after a Wellknown request // 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 -> is LoginViewEvents2.OnLoginModeNotSupported ->
onLoginModeNotSupported(event.supportedTypes) onLoginModeNotSupported(event.supportedTypes)
is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event) is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event)
is LoginViewEvents2.Finish -> terminate(true) is LoginViewEvents2.Finish -> terminate()
is LoginViewEvents2.CancelRegistration -> handleCancelRegistration() is LoginViewEvents2.CancelRegistration -> handleCancelRegistration()
} }
} }
@ -296,14 +296,13 @@ class Login2Variant(
option = commonOption option = commonOption
) )
} else { } else {
terminate(false) terminate()
} }
} }
private fun terminate(newAccount: Boolean) { private fun terminate() {
val intent = HomeActivity.newIntent( val intent = HomeActivity.newIntent(
activity, activity
accountCreation = newAccount
) )
activity.startActivity(intent) activity.startActivity(intent)
activity.finish() 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.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService 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.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.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
@ -127,7 +128,7 @@ class OnboardingViewModel @AssistedInject constructor(
val isRegistrationStarted: Boolean val isRegistrationStarted: Boolean
get() = authenticationService.isRegistrationStarted() get() = authenticationService.isRegistrationStarted()
private val loginWizard: LoginWizard? private val loginWizard: LoginWizard
get() = authenticationService.getLoginWizard() get() = authenticationService.getLoginWizard()
private var loginConfig: LoginConfig? = null private var loginConfig: LoginConfig? = null
@ -245,21 +246,15 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleLoginWithToken(action: OnboardingAction.LoginWithToken) { private fun handleLoginWithToken(action: OnboardingAction.LoginWithToken) {
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
setState { copy(isLoading = true) }
if (safeLoginWizard == null) { currentJob = viewModelScope.launch {
setState { copy(isLoading = false) } try {
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration"))) val result = safeLoginWizard.loginWithToken(action.loginToken)
} else { onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
setState { copy(isLoading = true) } } catch (failure: Throwable) {
setState { copy(isLoading = false) }
currentJob = viewModelScope.launch { _viewEvents.post(OnboardingViewEvents.Failure(failure))
try {
val result = safeLoginWizard.loginWithToken(action.loginToken)
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
} }
} }
} }
@ -289,7 +284,11 @@ class OnboardingViewModel @AssistedInject constructor(
// do nothing // do nothing
} }
else -> when (it) { 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.NextStep -> onFlowResponse(it.flowResult, onNextRegistrationStepAction)
is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email))
is RegistrationResult.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) 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 OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
private fun handleRegisterWith(action: AuthenticateAction.Register) { private fun handleRegisterWith(action: AuthenticateAction.Register) {
setState {
val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password)
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
}
reAuthHelper.data = action.password reAuthHelper.data = action.password
handleRegisterAction( handleRegisterAction(
RegisterAction.CreateAccount( RegisterAction.CreateAccount(
@ -368,7 +371,7 @@ class OnboardingViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
isLoading = false, isLoading = false,
resetPasswordEmail = null resetState = ResetState()
) )
} }
} }
@ -438,59 +441,52 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleResetPassword(action: OnboardingAction.ResetPassword) { private fun handleResetPassword(action: OnboardingAction.ResetPassword) {
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
setState { copy(isLoading = true) }
if (safeLoginWizard == null) { currentJob = viewModelScope.launch {
setState { copy(isLoading = false) } runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration"))) onSuccess = {
} else { setState {
setState { copy(isLoading = true) } copy(
isLoading = false,
currentJob = viewModelScope.launch { resetState = ResetState(email = action.email, newPassword = action.newPassword)
try { )
safeLoginWizard.resetPassword(action.email, action.newPassword) }
} catch (failure: Throwable) { _viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
setState { copy(isLoading = false) } },
_viewEvents.post(OnboardingViewEvents.Failure(failure)) onFailure = {
return@launch setState { copy(isLoading = false) }
} _viewEvents.post(OnboardingViewEvents.Failure(it))
}
setState { )
copy(
isLoading = false,
resetPasswordEmail = action.email
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
}
} }
} }
private fun handleResetPasswordMailConfirmed() { private fun handleResetPasswordMailConfirmed() {
val safeLoginWizard = loginWizard setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
if (safeLoginWizard == null) { val resetState = awaitState().resetState
setState { copy(isLoading = false) } when (val newPassword = resetState.newPassword) {
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration"))) null -> {
} else {
setState { copy(isLoading = false) }
currentJob = viewModelScope.launch {
try {
safeLoginWizard.resetPasswordMailConfirmed()
} catch (failure: Throwable) {
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure)) _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set")))
return@launch
} }
setState { else -> {
copy( runCatching { loginWizard.resetPasswordMailConfirmed(newPassword) }.fold(
isLoading = false, onSuccess = {
resetPasswordEmail = null 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) } setState { copy(isLoading = true) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
directLoginUseCase.execute(action, homeServerConnectionConfig).fold( directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
onSuccess = { onSessionCreated(it, isAccountCreated = false) }, onSuccess = { onSessionCreated(it, authenticationDescription = AuthenticationDescription.Login) },
onFailure = { onFailure = {
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(it)) _viewEvents.post(OnboardingViewEvents.Failure(it))
@ -510,25 +506,19 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleLogin(action: AuthenticateAction.Login) { private fun handleLogin(action: AuthenticateAction.Login) {
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
setState { copy(isLoading = true) }
if (safeLoginWizard == null) { currentJob = viewModelScope.launch {
setState { copy(isLoading = false) } try {
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Bad configuration"))) val result = safeLoginWizard.login(
} else { action.username,
setState { copy(isLoading = true) } action.password,
currentJob = viewModelScope.launch { action.initialDeviceName
try { )
val result = safeLoginWizard.login( reAuthHelper.data = action.password
action.username, onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
action.password, } catch (failure: Throwable) {
action.initialDeviceName setState { copy(isLoading = false) }
) _viewEvents.post(OnboardingViewEvents.Failure(failure))
reAuthHelper.data = action.password
onSessionCreated(result, isAccountCreated = false)
} catch (failure: Throwable) {
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
} }
} }
} }
@ -553,7 +543,7 @@ class OnboardingViewModel @AssistedInject constructor(
internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction) internalRegisterAction(RegisterAction.RegisterDummy, onNextRegistrationStepAction)
} }
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) { private suspend fun onSessionCreated(session: Session, authenticationDescription: AuthenticationDescription) {
val state = awaitState() val state = awaitState()
state.useCase?.let { useCase -> state.useCase?.let { useCase ->
session.vectorStore(applicationContext).setUseCase(useCase) session.vectorStore(applicationContext).setUseCase(useCase)
@ -564,15 +554,15 @@ class OnboardingViewModel @AssistedInject constructor(
authenticationService.reset() authenticationService.reset()
session.configureAndStart(applicationContext) session.configureAndStart(applicationContext)
when (isAccountCreated) { when (authenticationDescription) {
true -> { is AuthenticationDescription.Register -> {
val personalizationState = createPersonalizationState(session, state) val personalizationState = createPersonalizationState(session, state)
setState { setState {
copy(isLoading = false, personalizationState = personalizationState) copy(isLoading = false, personalizationState = personalizationState)
} }
_viewEvents.post(OnboardingViewEvents.OnAccountCreated) _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
} }
false -> { AuthenticationDescription.Login -> {
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
} }
@ -603,7 +593,7 @@ class OnboardingViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials) val result = authenticationService.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials)
onSessionCreated(result, isAccountCreated = false) onSessionCreated(result, authenticationDescription = AuthenticationDescription.Login)
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
} }
@ -745,8 +735,12 @@ class OnboardingViewModel @AssistedInject constructor(
return loginConfig?.homeServerUrl return loginConfig?.homeServerUrl
} }
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? {
return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) 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? { fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {

View file

@ -39,7 +39,7 @@ data class OnboardingViewState(
@PersistState @PersistState
val signMode: SignMode = SignMode.Unknown, val signMode: SignMode = SignMode.Unknown,
@PersistState @PersistState
val resetPasswordEmail: String? = null, val resetState: ResetState = ResetState(),
// For SSO session recovery // For SSO session recovery
@PersistState @PersistState
@ -51,6 +51,9 @@ data class OnboardingViewState(
@PersistState @PersistState
val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(), val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(),
@PersistState
val selectedAuthenticationState: SelectedAuthenticationState = SelectedAuthenticationState(),
@PersistState @PersistState
val personalizationState: PersonalizationState = PersonalizationState() val personalizationState: PersonalizationState = PersonalizationState()
) : MavericksState ) : MavericksState
@ -80,3 +83,14 @@ data class PersonalizationState(
fun supportsPersonalization() = supportsChangingDisplayName || supportsChangingProfilePicture 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 -> final override fun invalidate() = withState(viewModel) { state ->
// True when email is sent with success to the homeserver // True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() isResetPasswordStarted = state.resetState.email.isNullOrBlank().not()
updateWithState(state) updateWithState(state)
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -65,7 +65,14 @@ data class E2EWellKnownConfig(
* clients should fallback to the default value of: ["key", "passphrase"]. * clients should fallback to the default value of: ["key", "passphrase"].
*/ */
@Json(name = "secure_backup_setup_methods") @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) @JsonClass(generateAdapter = true)

View file

@ -16,6 +16,11 @@
package im.vector.app.features.raw.wellknown 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.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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 fun ElementWellKnown.isSecureBackupRequired() = elementE2E?.secureBackupRequired
?: riotE2E?.secureBackupRequired ?: riotE2E?.secureBackupRequired
?: false ?: false

View file

@ -151,12 +151,14 @@ class AudioWaveformView @JvmOverloads constructor(
private fun handleNewFftList(fftList: List<FFT>) { private fun handleNewFftList(fftList: List<FFT>) {
val maxVisibleBarCount = getMaxVisibleBarCount() val maxVisibleBarCount = getMaxVisibleBarCount()
fftList.forEach { fft -> fftList.forEach { fft ->
rawFftList.add(fft) rawFftList.add(fft)
val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight) val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight)
visibleBarHeights.add(FFT(barHeight, fft.color)) visibleBarHeights.add(FFT(barHeight, fft.color))
if (visibleBarHeights.size > maxVisibleBarCount) { 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_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:ellipsize="end" android:ellipsize="end"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
android:textStyle="bold" 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.FakeDirectLoginUseCase
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
import im.vector.app.test.fakes.FakeHomeServerHistoryService 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.FakeRegisterActionHandler
import im.vector.app.test.fakes.FakeRegistrationWizard import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession 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 const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password) 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 { class OnboardingViewModelTest {
@ -85,6 +88,7 @@ class OnboardingViewModelTest {
private val fakeHomeServerConnectionConfigFactory = FakeHomeServerConnectionConfigFactory() private val fakeHomeServerConnectionConfigFactory = FakeHomeServerConnectionConfigFactory()
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase() private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService() private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
private val fakeLoginWizard = FakeLoginWizard()
private var initialState = OnboardingViewState() private var initialState = OnboardingViewState()
private lateinit var viewModel: OnboardingViewModel private lateinit var viewModel: OnboardingViewModel
@ -466,6 +470,43 @@ class OnboardingViewModelTest {
.finish() .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) { private fun viewModelWith(state: OnboardingViewState) {
OnboardingViewModel( OnboardingViewModel(
state, 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.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig 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.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.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@ -36,6 +37,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
every { isRegistrationStarted() } returns started every { isRegistrationStarted() } returns started
} }
fun givenLoginWizard(loginWizard: LoginWizard) {
every { getLoginWizard() } returns loginWizard
}
fun givenLoginFlow(config: HomeServerConnectionConfig, result: LoginFlowResult) { fun givenLoginFlow(config: HomeServerConnectionConfig, result: LoginFlowResult) {
coEvery { getLoginFlow(config) } returns result 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) }
}
}