diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 50a9cdf5fc..7cb47fa952 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -23,7 +23,8 @@ body: ### Do the release - - [ ] Create release with gitflow, branch name `release/1.2.3` + - [ ] Make sure `develop` and `main` are up to date (git pull) + - [ ] Checkout develop and create a release with gitflow, branch name `release/1.2.3` - [ ] Check the crashes from the PlayStore - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0573461e7a..b6746c77d3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,8 @@ updates: directory: "/" schedule: interval: "weekly" + reviewers: + - "vector-im/element-android-reviewers" ignore: - dependency-name: "*github-script*" # Updates for Gradle dependencies used in the app @@ -19,6 +21,6 @@ updates: interval: "daily" open-pull-requests-limit: 200 reviewers: - - "bmarty" + - "vector-im/element-android-reviewers" ignore: - dependency-name: com.google.zxing:core diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..b6333c5940 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,22 @@ +name: Documentation + +on: + push: + branches: [ develop ] + +jobs: + docs: + name: Generate and publish Android Matrix SDK documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Build docs + run: ./gradlew dokkaHtml + + - name: Deploy docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./matrix-sdk-android/build/dokka/html diff --git a/.github/workflows/nightly.yml b/.github/workflows/post-pr.yml similarity index 84% rename from .github/workflows/nightly.yml rename to .github/workflows/post-pr.yml index 502e3e275f..54107475c7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/post-pr.yml @@ -1,28 +1,43 @@ -name: Nightly Tests +name: Integration Tests + +# This runs for all closed pull requests against main, including those closed without merge. +# Further filtering occurs in 'should-i-run' on: - push: - branches: [ release/* ] - schedule: - # At 20:00 every day UTC - - cron: '0 20 * * *' - workflow_dispatch: + pull_request: + types: [closed] + branches: [develop] # Enrich gradle.properties for CI/CD env: CI_GRADLE_ARG_PROPERTIES: > -Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.parallel=false + jobs: + + # More info on should-i-run: + # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the + # other jobs below, so none will run. + # except for the notification job at the bottom which will run all the time, unless should-i-run isn't + # successful, or all the other jobs have succeeded + + should-i-run: + name: Check if PR is suitable for analysis + runs-on: ubuntu-latest + if: github.event.pull_request.merged # Additionally require PR to have been completely merged. + steps: + - run: echo "Run those tests!" # no-op success + # Run Android Tests integration-tests: name: Matrix SDK - Running Integration Tests + needs: should-i-run runs-on: macos-latest strategy: fail-fast: false matrix: api-level: [ 28 ] - # No concurrency required, runs every time on a schedule. steps: - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 @@ -43,11 +58,12 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Start synapse server - uses: michaelkaye/setup-matrix-synapse@v1.0.1 + 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 @@ -209,6 +225,7 @@ jobs: ui-tests: name: UI Tests (Synapse) + needs: should-i-run runs-on: macos-latest strategy: fail-fast: false @@ -230,11 +247,12 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Start synapse server - uses: michaelkaye/setup-matrix-synapse@v1.0.1 + uses: michaelkaye/setup-matrix-synapse@v1.0.3 with: uploadLogs: true httpPort: 8080 disableRateLimiting: true + public_baseurl: "http://10.0.2.2:8080/" - uses: actions/setup-java@v3 with: distribution: 'adopt' @@ -266,7 +284,8 @@ jobs: codecov-units: name: Unit tests with code coverage - runs-on: macos-latest + needs: should-i-run + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 @@ -290,49 +309,21 @@ jobs: path: | build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml - sonarqube: - name: Sonarqube upload - runs-on: macos-latest - if: always() && github.event_name == 'schedule' - 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 scheduled runs, or pushes to the release branches, do not notify for manually triggered runs +# Notify the channel about delayed failures notify: name: Notify matrix runs-on: ubuntu-latest needs: + - should-i-run - integration-tests - ui-tests - - sonarqube - if: always() && github.event_name != 'workflow_dispatch' + - codecov-units + if: always() && (needs.should-i-run.result == 'success' ) && ((needs.codecov-units.result != 'success' ) || (needs.ui-tests.result != 'success') || (needs.integration-tests.result != 'success')) # No concurrency required, runs every time on a schedule. steps: - uses: michaelkaye/matrix-hookshot-action@v1.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }} - text_template: "{{#if '${{ github.event_name }}' == 'schedule' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" - html_template: "{{#if '${{ github.event_name }}' == 'schedule' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" + text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" + html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 0000000000..6809751d91 --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,81 @@ +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 }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" diff --git a/CHANGES.md b/CHANGES.md index f952ec952a..8e42149545 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,58 @@ +Changes in Element v1.4.14 (2022-05-05) +======================================= + +Features ✨ +---------- + - Improve management of ignored users ([#5772](https://github.com/vector-im/element-android/issues/5772)) + - VoIP Screen Sharing Permission ([#5811](https://github.com/vector-im/element-android/issues/5811)) + - Live location sharing: updating beacon state event content structure ([#5814](https://github.com/vector-im/element-android/issues/5814)) + +Bugfixes 🐛 +---------- + - Fixes crash when accepting or receiving VOIP calls ([#5421](https://github.com/vector-im/element-android/issues/5421)) + - Improve/fix crashes on messages decryption ([#5592](https://github.com/vector-im/element-android/issues/5592)) + - Tentative fix of images crashing when being sent or shared from gallery ([#5652](https://github.com/vector-im/element-android/issues/5652)) + - Improving deactivation experience along with a crash fix ([#5721](https://github.com/vector-im/element-android/issues/5721)) + - Adds missing suggested tag for rooms in Explore Space ([#5826](https://github.com/vector-im/element-android/issues/5826)) + - Fixes missing call icons when threads are enabled ([#5847](https://github.com/vector-im/element-android/issues/5847)) + - Fix UX freezing when creating secure backup ([#5871](https://github.com/vector-im/element-android/issues/5871)) + - Fixes sign in via other requiring homeserver registration to be enabled ([#5874](https://github.com/vector-im/element-android/issues/5874)) + - Don't pause timer when call is held. ([#5885](https://github.com/vector-im/element-android/issues/5885)) + - Fix UISIDetector grace period bug ([#5886](https://github.com/vector-im/element-android/issues/5886)) + - Fix a crash with space invitations in the space list, and do not display space invitation twice. ([#5924](https://github.com/vector-im/element-android/issues/5924)) + - Fixes crash on android api 21/22 devices when opening messages due to Konfetti library ([#5925](https://github.com/vector-im/element-android/issues/5925)) + +In development 🚧 +---------------- + - Reorders the registration steps to prioritise email, then terms for the FTUE onboarding ([#5783](https://github.com/vector-im/element-android/issues/5783)) + - [Live location sharing] Improve aggregation process of events ([#5862](https://github.com/vector-im/element-android/issues/5862)) + +Improved Documentation 📚 +------------------------ + - Update the PR process doc with 2 reviewers and a new reviewer team. ([#5836](https://github.com/vector-im/element-android/issues/5836)) + - Improve documentation of the project and of the SDK ([#5854](https://github.com/vector-im/element-android/issues/5854)) + +SDK API changes ⚠️ +------------------ + - Added registrationCustom into RegistrationWizard to send custom auth params for sign up + - Moved terms converter into api package to make it accessible in sdk ([#5575](https://github.com/vector-im/element-android/issues/5575)) + - Move package `org.matrix.android.sdk.api.pushrules` to `org.matrix.android.sdk.api.session.pushrules` ([#5812](https://github.com/vector-im/element-android/issues/5812)) + - Some `Session` apis are now available by requesting the service first. For instance `Session.updateAvatar(...)` is now `Session.profileService().updateAvatar(...)` + - The shortcut `Room.search()` has been removed, you have to use `Session.searchService().search()` ([#5816](https://github.com/vector-im/element-android/issues/5816)) + - Add return type to RoomApi.sendStateEvent() to retrieve the created event id ([#5855](https://github.com/vector-im/element-android/issues/5855)) + - `Room` apis are now available by requesting the service first. For instance `Room.updateAvatar(...)` is now `Room.stateService().updateAvatar(...)` ([#5858](https://github.com/vector-im/element-android/issues/5858)) + - Remove unecessary field `eventId` from `EventAnnotationsSummary` and `ReferencesAggregatedSummary` ([#5890](https://github.com/vector-im/element-android/issues/5890)) + - Replace usage of `System.currentTimeMillis()` by a `Clock` interface ([#5907](https://github.com/vector-im/element-android/issues/5907)) + +Other changes +------------- + - Move "Ignored users" setting section into "Security & Privacy" ([#5773](https://github.com/vector-im/element-android/issues/5773)) + - Add a picto for ignored users in the room member list screen ([#5774](https://github.com/vector-im/element-android/issues/5774)) + - Autoformats entire project ([#5805](https://github.com/vector-im/element-android/issues/5805)) + - Add a GH workflow to push ElementX issues to the global board. ([#5832](https://github.com/vector-im/element-android/issues/5832)) + - Faster Olm decrypt when there is a lot of existing sessions ([#5872](https://github.com/vector-im/element-android/issues/5872)) + + Changes in Element 1.4.13 (2022-04-26) ====================================== diff --git a/README.md b/README.md index 8306fd8593..54dfb7b288 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](https://f-droid.org/app/im.vector.app) -Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml) +Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml) # New Android SDK @@ -53,3 +53,4 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/ Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process). We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues. + diff --git a/build.gradle b/build.gradle index 1d86f482da..fa26638015 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,17 @@ buildscript { apply from: 'dependencies_groups.gradle' repositories { - google() + // Do not use `google()`, it prevents Dependabot from working properly + maven { + url 'https://maven.google.com' + } maven { url "https://plugins.gradle.org/m2/" } + // Do not use `mavenCentral()`, it prevents Dependabot from working properly + maven { + url 'https://repo1.maven.org/maven2' + } } dependencies { @@ -47,7 +54,9 @@ allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" repositories { - mavenCentral { + // Do not use `mavenCentral()`, it prevents Dependabot from working properly + maven { + url 'https://repo1.maven.org/maven2' content { groups.mavenCentral.regex.each { includeGroupByRegex it } groups.mavenCentral.group.each { includeGroup it } @@ -70,14 +79,18 @@ allprojects { groups.jitsi.group.each { includeGroup it } } } - google { + // Do not use `google()`, it prevents Dependabot from working properly + maven { + url 'https://maven.google.com' content { groups.google.regex.each { includeGroupByRegex it } groups.google.group.each { includeGroup it } } } //noinspection JcenterRepositoryObsolete - jcenter { + // Do not use `jcenter`, it prevents Dependabot from working properly + maven { + url 'https://jcenter.bintray.com' content { groups.jcenter.regex.each { includeGroupByRegex it } groups.jcenter.group.each { includeGroup it } diff --git a/changelog.d/46312.misc b/changelog.d/46312.misc new file mode 100644 index 0000000000..5e0112372f --- /dev/null +++ b/changelog.d/46312.misc @@ -0,0 +1 @@ +Notify the user for each new message diff --git a/changelog.d/5151.misc b/changelog.d/5151.misc new file mode 100644 index 0000000000..b785c4229c --- /dev/null +++ b/changelog.d/5151.misc @@ -0,0 +1 @@ +Improve threads rendering in the main timeline diff --git a/changelog.d/5421.bugfix b/changelog.d/5421.bugfix deleted file mode 100644 index 2f9a1c0b1c..0000000000 --- a/changelog.d/5421.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes crash when accepting or receiving VOIP calls diff --git a/changelog.d/5494.feature b/changelog.d/5494.feature new file mode 100644 index 0000000000..59b8a78a2c --- /dev/null +++ b/changelog.d/5494.feature @@ -0,0 +1 @@ +Use key backup before requesting keys + refactor & improvement of key request/forward \ No newline at end of file diff --git a/changelog.d/5559.sdk b/changelog.d/5559.sdk new file mode 100644 index 0000000000..2466fcef48 --- /dev/null +++ b/changelog.d/5559.sdk @@ -0,0 +1,4 @@ +- New API to enable/disable key forwarding CryptoService#enableKeyGossiping() +- New API to limit room key request only to own devices MXCryptoConfig#limitRoomKeyRequestsToMyDevices +- Event Trail API has changed, now using AuditTrail events +- New API to manually accept an incoming key request CryptoService#manuallyAcceptRoomKeyRequest() diff --git a/changelog.d/5575.sdk b/changelog.d/5575.sdk deleted file mode 100644 index 19339bdce2..0000000000 --- a/changelog.d/5575.sdk +++ /dev/null @@ -1,2 +0,0 @@ -- Added registrationCustom into RegistrationWizard to send custom auth params for sign up -- Moved terms converter into api package to make it accessible in sdk \ No newline at end of file diff --git a/changelog.d/5592.bugfix b/changelog.d/5592.bugfix deleted file mode 100644 index f0df3dc646..0000000000 --- a/changelog.d/5592.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improve/fix crashes on messages decryption diff --git a/changelog.d/5652.bugfix b/changelog.d/5652.bugfix deleted file mode 100644 index 8ebea1558f..0000000000 --- a/changelog.d/5652.bugfix +++ /dev/null @@ -1 +0,0 @@ -Tentative fix of images crashing when being sent or shared from gallery diff --git a/changelog.d/5721.bugfix b/changelog.d/5721.bugfix deleted file mode 100644 index 8b752b43a9..0000000000 --- a/changelog.d/5721.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improving deactivation experience along with a crash fix \ No newline at end of file diff --git a/changelog.d/5772.feature b/changelog.d/5772.feature deleted file mode 100644 index 85eec0a1ad..0000000000 --- a/changelog.d/5772.feature +++ /dev/null @@ -1 +0,0 @@ -Improve management of ignored users \ No newline at end of file diff --git a/changelog.d/5773.misc b/changelog.d/5773.misc deleted file mode 100644 index 39c8b42073..0000000000 --- a/changelog.d/5773.misc +++ /dev/null @@ -1 +0,0 @@ -Move "Ignored users" setting section into "Security & Privacy" \ No newline at end of file diff --git a/changelog.d/5774.misc b/changelog.d/5774.misc deleted file mode 100644 index 795106381b..0000000000 --- a/changelog.d/5774.misc +++ /dev/null @@ -1 +0,0 @@ -Add a picto for ignored users in the room member list screen \ No newline at end of file diff --git a/changelog.d/5783.wip b/changelog.d/5783.wip deleted file mode 100644 index e306c0f217..0000000000 --- a/changelog.d/5783.wip +++ /dev/null @@ -1 +0,0 @@ -Reorders the registration steps to prioritise email, then terms for the FTUE onboarding diff --git a/changelog.d/5805.misc b/changelog.d/5805.misc deleted file mode 100644 index e0e6a311b4..0000000000 --- a/changelog.d/5805.misc +++ /dev/null @@ -1 +0,0 @@ -Autoformats entire project diff --git a/changelog.d/5811.feature b/changelog.d/5811.feature deleted file mode 100644 index 12111e323b..0000000000 --- a/changelog.d/5811.feature +++ /dev/null @@ -1 +0,0 @@ -VoIP Screen Sharing Permission \ No newline at end of file diff --git a/changelog.d/5812.sdk b/changelog.d/5812.sdk deleted file mode 100644 index 5ddd8cfac1..0000000000 --- a/changelog.d/5812.sdk +++ /dev/null @@ -1 +0,0 @@ -Move package `org.matrix.android.sdk.api.pushrules` to `org.matrix.android.sdk.api.session.pushrules` diff --git a/changelog.d/5814.feature b/changelog.d/5814.feature deleted file mode 100644 index c892702486..0000000000 --- a/changelog.d/5814.feature +++ /dev/null @@ -1 +0,0 @@ -Live location sharing: updating beacon state event content structure diff --git a/changelog.d/5816.sdk b/changelog.d/5816.sdk deleted file mode 100644 index 17233c66e5..0000000000 --- a/changelog.d/5816.sdk +++ /dev/null @@ -1,2 +0,0 @@ -Some `Session` apis are now available by requesting the service first. For instance `Session.updateAvatar(...)` is now `Session.profileService().updateAvatar(...)` -The shortcut `Room.search()` has been removed, you have to use `Session.searchService().search()` diff --git a/changelog.d/5825.bugfix b/changelog.d/5825.bugfix new file mode 100644 index 0000000000..77560027ba --- /dev/null +++ b/changelog.d/5825.bugfix @@ -0,0 +1 @@ +Changed copy and list order in member profile screen. \ No newline at end of file diff --git a/changelog.d/5826.bugfix b/changelog.d/5826.bugfix deleted file mode 100644 index 2735568f73..0000000000 --- a/changelog.d/5826.bugfix +++ /dev/null @@ -1 +0,0 @@ -Adds missing suggested tag for rooms in Explore Space diff --git a/changelog.d/5832.misc b/changelog.d/5832.misc deleted file mode 100644 index ace9dff5d4..0000000000 --- a/changelog.d/5832.misc +++ /dev/null @@ -1 +0,0 @@ -Add a GH workflow to push ElementX issues to the global board. \ No newline at end of file diff --git a/changelog.d/5836.doc b/changelog.d/5836.doc deleted file mode 100644 index 42073d66ef..0000000000 --- a/changelog.d/5836.doc +++ /dev/null @@ -1 +0,0 @@ -Update the PR process doc with 2 reviewers and a new reviewer team. \ No newline at end of file diff --git a/changelog.d/5847.bugfix b/changelog.d/5847.bugfix deleted file mode 100644 index acd13dec9a..0000000000 --- a/changelog.d/5847.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes missing call icons when threads are enabled diff --git a/changelog.d/5854.doc b/changelog.d/5854.doc deleted file mode 100644 index 38a1c756af..0000000000 --- a/changelog.d/5854.doc +++ /dev/null @@ -1 +0,0 @@ -Improve documentation of the project and of the SDK diff --git a/changelog.d/5855.sdk b/changelog.d/5855.sdk deleted file mode 100644 index bad5a11398..0000000000 --- a/changelog.d/5855.sdk +++ /dev/null @@ -1 +0,0 @@ -- Add return type to RoomApi.sendStateEvent() to retrieve the created event id diff --git a/changelog.d/5858.sdk b/changelog.d/5858.sdk deleted file mode 100644 index 9f2e7ef0f2..0000000000 --- a/changelog.d/5858.sdk +++ /dev/null @@ -1 +0,0 @@ -`Room` apis are now available by requesting the service first. For instance `Room.updateAvatar(...)` is now `Room.stateService().updateAvatar(...)` diff --git a/changelog.d/5862.wip b/changelog.d/5862.wip deleted file mode 100644 index 303a054c5e..0000000000 --- a/changelog.d/5862.wip +++ /dev/null @@ -1 +0,0 @@ -[Live location sharing] Improve aggregation process of events diff --git a/changelog.d/5871.bugfix b/changelog.d/5871.bugfix deleted file mode 100644 index b223ddd141..0000000000 --- a/changelog.d/5871.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix UX freezing when creating secure backup diff --git a/changelog.d/5872.misc b/changelog.d/5872.misc deleted file mode 100644 index 1e15229800..0000000000 --- a/changelog.d/5872.misc +++ /dev/null @@ -1 +0,0 @@ -Faster Olm decrypt when there is a lot of existing sessions diff --git a/changelog.d/5886.bugfix b/changelog.d/5886.bugfix deleted file mode 100644 index c4239e5c3a..0000000000 --- a/changelog.d/5886.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix UISIDetector grace period bug diff --git a/changelog.d/5890.sdk b/changelog.d/5890.sdk deleted file mode 100644 index 0d5e8d783d..0000000000 --- a/changelog.d/5890.sdk +++ /dev/null @@ -1 +0,0 @@ -Remove unecessary field `eventId` from `EventAnnotationsSummary` and `ReferencesAggregatedSummary` diff --git a/changelog.d/5911.feature b/changelog.d/5911.feature new file mode 100644 index 0000000000..368a3b4056 --- /dev/null +++ b/changelog.d/5911.feature @@ -0,0 +1 @@ +Screen sharing over WebRTC diff --git a/changelog.d/5925.bugfix b/changelog.d/5925.bugfix deleted file mode 100644 index eff0c17a5c..0000000000 --- a/changelog.d/5925.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes crash on android api 21/22 devices when opening messages due to Konfetti library diff --git a/changelog.d/5936.feature b/changelog.d/5936.feature new file mode 100644 index 0000000000..cbf14aaba1 --- /dev/null +++ b/changelog.d/5936.feature @@ -0,0 +1 @@ +Added themed launch icons for Android 13 \ No newline at end of file diff --git a/changelog.d/5941.bugfix b/changelog.d/5941.bugfix new file mode 100644 index 0000000000..0ea17668c6 --- /dev/null +++ b/changelog.d/5941.bugfix @@ -0,0 +1 @@ +If animations are disable on the System, chat effects and confetti will be disabled too diff --git a/changelog.d/5953.misc b/changelog.d/5953.misc new file mode 100644 index 0000000000..a3ad5dae93 --- /dev/null +++ b/changelog.d/5953.misc @@ -0,0 +1 @@ +Reformatted project code diff --git a/changelog.d/5965.sdk b/changelog.d/5965.sdk new file mode 100644 index 0000000000..5bb6c3aac4 --- /dev/null +++ b/changelog.d/5965.sdk @@ -0,0 +1 @@ +Including SSL/TLS error handing when doing WellKnown lookups without a custom HomeServerConnectionConfig diff --git a/changelog.d/5997.misc b/changelog.d/5997.misc new file mode 100644 index 0000000000..328f3c0079 --- /dev/null +++ b/changelog.d/5997.misc @@ -0,0 +1 @@ +Update check for server-side threads support to match spec. diff --git a/dependencies.gradle b/dependencies.gradle index 7666a3bf9f..90990810a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,26 +7,26 @@ ext.versions = [ 'targetCompat' : JavaVersion.VERSION_11, ] -def gradle = "7.0.4" +def gradle = "7.2.0" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.6.0" +def kotlin = "1.6.21" def kotlinCoroutines = "1.6.0" -def dagger = "2.40.5" +def dagger = "2.42" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" def moshi = "1.13.0" -def lifecycle = "2.4.0" +def lifecycle = "2.4.1" def flowBinding = "1.2.0" def epoxy = "4.6.2" -def mavericks = "2.5.0" -def glide = "4.12.0" +def mavericks = "2.6.1" +def glide = "4.13.2" def bigImageViewer = "1.8.1" -def jjwt = "0.11.2" -def vanniktechEmoji = "0.8.0" +def jjwt = "0.11.5" +def vanniktechEmoji = "0.9.0" // Testing -def mockk = "1.12.1" +def mockk = "1.12.4" def espresso = "3.4.0" def androidxTest = "1.4.0" def androidxOrchestrator = "1.4.1" @@ -45,15 +45,15 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'appCompat' : "androidx.appcompat:appcompat:1.4.0", + 'appCompat' : "androidx.appcompat:appcompat:1.4.1", 'core' : "androidx.core:core-ktx:1.7.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", - 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.0", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.2", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3", 'work' : "androidx.work:work-runtime-ktx:2.7.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", - 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", + 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", 'junit' : "androidx.test.ext:junit:1.1.3", 'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle", 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle", @@ -72,7 +72,7 @@ ext.libs = [ 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" ], google : [ - 'material' : "com.google.android.material:material:1.5.0" + 'material' : "com.google.android.material:material:1.6.0" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", diff --git a/docs/ui-tests.md b/docs/ui-tests.md index 05eb50f525..667a6ed7fb 100644 --- a/docs/ui-tests.md +++ b/docs/ui-tests.md @@ -176,4 +176,4 @@ class SettingsAdvancedRobot { clickOn(R.string.settings_developer_mode_summary) } } -``` \ No newline at end of file +``` diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt new file mode 100644 index 0000000000..46a75b77a7 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Posun přehrávání v hlasových zprávách. Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40104140.txt b/fastlane/metadata/android/en-US/changelogs/40104140.txt new file mode 100644 index 0000000000..5cbd25f4d3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Main changes in this version: Improve management of ignored users. Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/es-ES/changelogs/40103030.txt b/fastlane/metadata/android/es-ES/changelogs/40103030.txt new file mode 100644 index 0000000000..36f59a2308 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Cambios principales en esta versión: Hacer la política de servidores de indentidad visible en los ajusted. Temporalmente quitar soporte para Android Auto. +Registro de cambios: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/es-ES/changelogs/40104110.txt b/fastlane/metadata/android/es-ES/changelogs/40104110.txt new file mode 100644 index 0000000000..281df19388 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Cambios principales en esta versión: Varias correciones de bugs y mejoras en la estabilidad +Registro de cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index fdba15e90e..0e484158d9 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -37,3 +37,6 @@ Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido Continúa donde lo dejaste Manténgase en contacto donde quiera que esté con el historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io + +Código abierto +Element Android es un proyecto de código abierto, alojado en GitHub. Porfavor, reporta bugs y problemas en esta dirección: https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/et/changelogs/40104100.txt b/fastlane/metadata/android/et/changelogs/40104100.txt new file mode 100644 index 0000000000..b164935ba4 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: kerimine häälsõnumites ning erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104110.txt b/fastlane/metadata/android/et/changelogs/40104110.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104100.txt b/fastlane/metadata/android/fa/changelogs/40104100.txt new file mode 100644 index 0000000000..e7cdca6641 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104100.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: لغزش در پیام‌های صوتی. رفع اشکال‌های مختلف و بهبودهای امنیتی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104110.txt b/fastlane/metadata/android/fa/changelogs/40104110.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104110.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104100.txt b/fastlane/metadata/android/id/changelogs/40104100.txt new file mode 100644 index 0000000000..3cda40aecc --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Gulir di pesan suara. Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104110.txt b/fastlane/metadata/android/id/changelogs/40104110.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104100.txt b/fastlane/metadata/android/it-IT/changelogs/40104100.txt new file mode 100644 index 0000000000..6376e6c45a --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: scorrimento nei vocali. Varie correzioni e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104110.txt b/fastlane/metadata/android/it-IT/changelogs/40104110.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40100100.txt b/fastlane/metadata/android/lo/changelogs/40100100.txt new file mode 100644 index 0000000000..fe0ee08327 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100100.txt @@ -0,0 +1,2 @@ +ສະບັບໃຫມ່ນີ້ສ່ວນໃຫຍ່ແມ່ນມີການແກ້ໄຂແລະການປັບປຸງ bug. ຕອນນີ້ການສົ່ງຂໍ້ຄວາມແມ່ນໄວຂຶ້ນຫຼາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/lo/changelogs/40100110.txt b/fastlane/metadata/android/lo/changelogs/40100110.txt new file mode 100644 index 0000000000..69139783ad --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100110.txt @@ -0,0 +1,2 @@ +ຮຸ່ນໃຫມ່ນີ້ສ່ວນໃຫຍ່ແມ່ນປະກອບດ້ວຍໜ້າຕາແອັບແລະການປັບປຸງປະສົບການຂອງຜູ້ໃຊ້. ດຽວນີ້ທ່ານສາມາດເຊີນໝູ່, ແລະສ້າງການສົນທະນາກົງໄດ້ໄວຂຶ້ນໂດຍການສະແກນລະຫັດ QR. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/lo/changelogs/40100120.txt b/fastlane/metadata/android/lo/changelogs/40100120.txt new file mode 100644 index 0000000000..256cd1a437 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວຢ່າງ URL, ແປ້ນພິມ Emoji ໃໝ່, ຄວາມສາມາດໃນການຕັ້ງຄ່າຫ້ອງໃໝ່, ແລະຫິມະສຳລັບວັນຄຣິດສະມາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/lo/changelogs/40100130.txt b/fastlane/metadata/android/lo/changelogs/40100130.txt new file mode 100644 index 0000000000..c4878ea5f6 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວຢ່າງ URL, ແປ້ນພິມ Emoji ໃໝ່, ຄວາມສາມາດໃນການຕັ້ງຄ່າຫ້ອງໃໝ່, ແລະຫິມະສຳລັບວັນຄຣິດສະມາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/lo/changelogs/40100140.txt b/fastlane/metadata/android/lo/changelogs/40100140.txt new file mode 100644 index 0000000000..591b9b94b3 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂການອະນຸຍາດຫ້ອງ, ຮູບແບບ ແຈ້ງ / ມືດອັດຕະໂນມັດ, ແລະການແກ້ໄຂບັນຫາຈຳນວນໜຶ່ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/lo/changelogs/40100150.txt b/fastlane/metadata/android/lo/changelogs/40100150.txt new file mode 100644 index 0000000000..98577742c7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮັບຮອງການເຂົ້າລະບົບດ້ວຍສື່ສັງຄົມອອນລາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/lo/changelogs/40100160.txt b/fastlane/metadata/android/lo/changelogs/40100160.txt new file mode 100644 index 0000000000..c369fc88d3 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮັບຮອງການເຂົ້າລະບົບດ້ວຍສື່ສັງຄົມອອນລາຍ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/lo/changelogs/40100170.txt b/fastlane/metadata/android/lo/changelogs/40100170.txt new file mode 100644 index 0000000000..776e1b6082 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40100170.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/lo/changelogs/40101000.txt b/fastlane/metadata/android/lo/changelogs/40101000.txt new file mode 100644 index 0000000000..f7d621404a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບຂອງ VoIP (ໂທສຽງ ແລະໂທວີດີໂອໃນການສົນທະນາກົງ) ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/lo/changelogs/40101010.txt b/fastlane/metadata/android/lo/changelogs/40101010.txt new file mode 100644 index 0000000000..e2e1778b7a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/lo/changelogs/40101020.txt b/fastlane/metadata/android/lo/changelogs/40101020.txt new file mode 100644 index 0000000000..581db95e90 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/lo/changelogs/40101030.txt b/fastlane/metadata/android/lo/changelogs/40101030.txt new file mode 100644 index 0000000000..46d861995f --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101030.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/lo/changelogs/40101040.txt b/fastlane/metadata/android/lo/changelogs/40101040.txt new file mode 100644 index 0000000000..8ea7cac4f4 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງປະສິດທິພາບ ແລະແກ້ໄຂຂໍ້ຜິດພາດ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/lo/changelogs/40101050.txt b/fastlane/metadata/android/lo/changelogs/40101050.txt new file mode 100644 index 0000000000..2823c04b06 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101050.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ບັນຫາດ່ວນສໍາລັບ 1.1.4 +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/lo/changelogs/40101060.txt b/fastlane/metadata/android/lo/changelogs/40101060.txt new file mode 100644 index 0000000000..0cdea9e2c7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ບັນຫາດ່ວນສໍາລັບ 1.1.5 +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/lo/changelogs/40101070.txt b/fastlane/metadata/android/lo/changelogs/40101070.txt new file mode 100644 index 0000000000..08926229f9 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຮອງຮັບເບຕ້າສຳລັບ Spaces. ບີບອັດວິດີໂອກ່ອນທີ່ຈະສົ່ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/lo/changelogs/40101080.txt b/fastlane/metadata/android/lo/changelogs/40101080.txt new file mode 100644 index 0000000000..f2947aae63 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປັບປຸງສໍາລັບ Spaces. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/lo/changelogs/40101090.txt b/fastlane/metadata/android/lo/changelogs/40101090.txt new file mode 100644 index 0000000000..129f86c562 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101090.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮັບຮອງສໍາລັບເຄືອຂ່າຍ gitter.im. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/lo/changelogs/40101100.txt b/fastlane/metadata/android/lo/changelogs/40101100.txt new file mode 100644 index 0000000000..337137d3bc --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບສີສັນ ແລະຄຸນສົມບັດໃໝ່ສຳລັບຊ່ອງຫວ່າງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/lo/changelogs/40101110.txt b/fastlane/metadata/android/lo/changelogs/40101110.txt new file mode 100644 index 0000000000..f95972eaa9 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບສີສັນ ແລະຄຸນສົມບັດໃໝ່ສຳລັບຊ່ອງຫວ່າງ (bugfix ສຳລັບ 1.1.10) +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/lo/changelogs/40101120.txt b/fastlane/metadata/android/lo/changelogs/40101120.txt new file mode 100644 index 0000000000..a613e176aa --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ອັບເດດຮູບແບບສີສັນ ແລະຮູບແບບ ແລະແກ້ໄຂການຂັດຂ້ອງຫຼັງຈາກການໂທວິດີໂອ +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/lo/changelogs/40101130.txt b/fastlane/metadata/android/lo/changelogs/40101130.txt new file mode 100644 index 0000000000..3cc85bc6f8 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັນຫາກ່ຽວຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/lo/changelogs/40101140.txt b/fastlane/metadata/android/lo/changelogs/40101140.txt new file mode 100644 index 0000000000..11a5df1059 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັນຫາກ່ຽວກັບຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/lo/changelogs/40101150.txt b/fastlane/metadata/android/lo/changelogs/40101150.txt new file mode 100644 index 0000000000..8751496a49 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປະຕິບັດຂໍ້ຄວາມສຽງພາຍໃຕ້ການຕັ້ງຄ່າຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/lo/changelogs/40101160.txt b/fastlane/metadata/android/lo/changelogs/40101160.txt new file mode 100644 index 0000000000..c102140102 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40101160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂຂໍ້ຜິດພາດໃນເວລາສົ່ງຂໍ້ຄວາມເຂົ້າລະຫັດຖ້າມີຄົນຢູ່ໃນຫ້ອງອອກຈາກລະບົບ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/lo/changelogs/40102000.txt b/fastlane/metadata/android/lo/changelogs/40102000.txt new file mode 100644 index 0000000000..69f1667b93 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40102000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການສົນທະນາດ້ວຍສຽງເປີດໄວ້ໂດຍມາດຕະຖານ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/lo/changelogs/40102010.txt b/fastlane/metadata/android/lo/changelogs/40102010.txt new file mode 100644 index 0000000000..237b48ba3b --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40102010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປັບປຸງຫຼາຍຢ່າງກ່ຽວກັບ VoIP ແລະ Spaces (ຍັງຢູ່ໃນເບຕ້າ). +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/lo/changelogs/40103000.txt b/fastlane/metadata/android/lo/changelogs/40103000.txt new file mode 100644 index 0000000000..8e386df4a7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຈັດລະບຽບຫ້ອງຂອງທ່ານໂດຍໃຊ້ Spaces! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/lo/changelogs/40103010.txt b/fastlane/metadata/android/lo/changelogs/40103010.txt new file mode 100644 index 0000000000..0a6a6461fd --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103010.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຈັດລະບຽບຫ້ອງຂອງທ່ານໂດຍໃຊ້ Spaces! v1.3.1 ກຳລັງແກ້ໄຂບັນແອັບໃຊ້ບໍ່ໄດ້ທີ່ສາມາດເກີດຂຶ້ນໃນ v1.3.0. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/lo/changelogs/40103020.txt b/fastlane/metadata/android/lo/changelogs/40103020.txt new file mode 100644 index 0000000000..8b9ed12a3a --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບ Android Auto. ການແກ້ໄຂຍັກຫຼາຍອັນ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/lo/changelogs/40103030.txt b/fastlane/metadata/android/lo/changelogs/40103030.txt new file mode 100644 index 0000000000..6374f4fdf5 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103030.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເຮັດໃຫ້ນະໂຍບາຍຂອງເຄື່ອງແມ່ຂ່າຍຢັ້ງຢືນຕົວຕົນ (ຫຼາຍໜ່ວຍ) ໃຫ້ເຫັນຢູ່ໃນການຕັ້ງຄ່າ. ລຶບການຮອງຮັບ Android Auto ອອກຊົ່ວຄາວ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/lo/changelogs/40103040.txt b/fastlane/metadata/android/lo/changelogs/40103040.txt new file mode 100644 index 0000000000..896d9190ce --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/lo/changelogs/40103050.txt b/fastlane/metadata/android/lo/changelogs/40103050.txt new file mode 100644 index 0000000000..b76db1a80c --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103050.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/lo/changelogs/40103060.txt b/fastlane/metadata/android/lo/changelogs/40103060.txt new file mode 100644 index 0000000000..2e97183a56 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບການມີຢູ່, ສໍາລັບຫ້ອງຂໍ້ຄວາມໂດຍກົງ (ໝາຍເຫດ: ການມີຢູ່ຖືກປິດການໃຊ້ງານຢູ່ໃນ matrix.org). ເພີ່ມການຮອງຮັບ Android Auto ອີກຄັ້ງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/lo/changelogs/40103070.txt b/fastlane/metadata/android/lo/changelogs/40103070.txt new file mode 100644 index 0000000000..47eb6c7b4d --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກສ່ວນໃຫຍ່ກ່ຽວກັບການແຈ້ງເຕືອນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/lo/changelogs/40103080.txt b/fastlane/metadata/android/lo/changelogs/40103080.txt new file mode 100644 index 0000000000..a12d374d20 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/lo/changelogs/40103090.txt b/fastlane/metadata/android/lo/changelogs/40103090.txt new file mode 100644 index 0000000000..ee93ca0a96 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103090.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບສະບັບຮ່າງການສົນທະນາດ້ວຍສຽງ. ແກ້ໄຂບັກຫຼາຍໆອັນ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.09 diff --git a/fastlane/metadata/android/lo/changelogs/40103100.txt b/fastlane/metadata/android/lo/changelogs/40103100.txt new file mode 100644 index 0000000000..b8b65387ef --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການຮອງຮັບແບບສໍາຫຼວດ (ໃນຫ້ອງທົດລອງ). ການອອກແບບຕົວຢ່າງ URL ໃໝ່. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/lo/changelogs/40103110.txt b/fastlane/metadata/android/lo/changelogs/40103110.txt new file mode 100644 index 0000000000..f64ee3b469 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ! +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/lo/changelogs/40103120.txt b/fastlane/metadata/android/lo/changelogs/40103120.txt new file mode 100644 index 0000000000..a7b8412cfb --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103120.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກ!. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/lo/changelogs/40103130.txt b/fastlane/metadata/android/lo/changelogs/40103130.txt new file mode 100644 index 0000000000..cb725ee68c --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103130.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.13 diff --git a/fastlane/metadata/android/lo/changelogs/40103140.txt b/fastlane/metadata/android/lo/changelogs/40103140.txt new file mode 100644 index 0000000000..1f72757cf1 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.14 diff --git a/fastlane/metadata/android/lo/changelogs/40103150.txt b/fastlane/metadata/android/lo/changelogs/40103150.txt new file mode 100644 index 0000000000..c11acd15d6 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103150.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປ່ຽນແປງຄັ້ງທໍາອິດໃນຫນ້າຈໍເລີ່ມຕົ້ນ, ລວມທັງການວິເຄາະເລືອກເຂົ້າຮ່ວມ. ຮອງຮັບເຫດການທີ່ມີຄະນິດສາດເພີ່ມຢູ່ໃນຫ້ອງທົດລອງ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.15 diff --git a/fastlane/metadata/android/lo/changelogs/40103160.txt b/fastlane/metadata/android/lo/changelogs/40103160.txt new file mode 100644 index 0000000000..a17f218463 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103160.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.16 diff --git a/fastlane/metadata/android/lo/changelogs/40103170.txt b/fastlane/metadata/android/lo/changelogs/40103170.txt new file mode 100644 index 0000000000..b8337a19cc --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103170.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.17 diff --git a/fastlane/metadata/android/lo/changelogs/40103180.txt b/fastlane/metadata/android/lo/changelogs/40103180.txt new file mode 100644 index 0000000000..e6b65ff812 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40103180.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ສົ່ງສະຖານທີ່ຂອງທ່ານໄປຫາຫ້ອງໃດກໍໄດ້. ແກ້ໄຂແບບສຳຫຼວດ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.3.18 diff --git a/fastlane/metadata/android/lo/changelogs/40104000.txt b/fastlane/metadata/android/lo/changelogs/40104000.txt new file mode 100644 index 0000000000..9d849da5b7 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104000.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການປະຕິບັດເບື້ອງຕົ້ນຂອງຂໍ້ຄວາມແບບກະທູ້. ຂໍ້ຄວາມຮູບແບບປຸມເປົ້າ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.0 diff --git a/fastlane/metadata/android/lo/changelogs/40104020.txt b/fastlane/metadata/android/lo/changelogs/40104020.txt new file mode 100644 index 0000000000..358ca7fd39 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104020.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເພີ່ມການສະຫນັບສະຫນູນກັບ @room ແລະແບບສໍາຫຼວດທີ່ບໍ່ເປີດເຜີຍໃນບັນດາການປ່ຽນແປງເລັກນ້ອຍອື່ນໆ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.2 diff --git a/fastlane/metadata/android/lo/changelogs/40104040.txt b/fastlane/metadata/android/lo/changelogs/40104040.txt new file mode 100644 index 0000000000..7bfaf76bf2 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104040.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ຕົວສະແດງການພິມ ແລະປັບປັງ IU. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.4 diff --git a/fastlane/metadata/android/lo/changelogs/40104060.txt b/fastlane/metadata/android/lo/changelogs/40104060.txt new file mode 100644 index 0000000000..10ae599698 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104060.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ທາມລາຍກະທູ້ ປະຈຸບັນເປັນແບບຖ່າຍທອດສົດ ແລະໄວຂຶ້ນ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.6 diff --git a/fastlane/metadata/android/lo/changelogs/40104070.txt b/fastlane/metadata/android/lo/changelogs/40104070.txt new file mode 100644 index 0000000000..4ede801350 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104070.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases/tag/v1.4.7 diff --git a/fastlane/metadata/android/lo/changelogs/40104080.txt b/fastlane/metadata/android/lo/changelogs/40104080.txt new file mode 100644 index 0000000000..5d7de22065 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104080.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເລື່ອນໃນຂໍ້ຄວາມສຽງ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40104100.txt b/fastlane/metadata/android/lo/changelogs/40104100.txt new file mode 100644 index 0000000000..5d7de22065 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104100.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ເລື່ອນໃນຂໍ້ຄວາມສຽງ. ແກ້ໄຂບັກຫຼາຍບ່ອນ ແລະ ປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/changelogs/40104110.txt b/fastlane/metadata/android/lo/changelogs/40104110.txt new file mode 100644 index 0000000000..edc025cb39 --- /dev/null +++ b/fastlane/metadata/android/lo/changelogs/40104110.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ການແກ້ໄຂ bug ຕ່າງໆແລະການປັບປຸງຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo/full_description.txt b/fastlane/metadata/android/lo/full_description.txt new file mode 100644 index 0000000000..094c1b57f5 --- /dev/null +++ b/fastlane/metadata/android/lo/full_description.txt @@ -0,0 +1,42 @@ +ລະບົບແຊັດນີ້ ມີຄວາມປອດໄພ ແລະເປັນແອັບທີ່ເຮັດຊ່ວຍສົ່ງເສີ່ມການເຮັດວຽກຮ່ວມກັນຢ່າງມີປະສິດທິຜົນໝາະສົມສຳລັບການສົນທະນາກຸ່ມໃນຂະນະທີ່ເຮັດວຽກທາງໄກ. ແອັບສົນທະນານີ້ໃຊ້ການເຂົ້າລະຫັດແບບຕົ້ນທາງຈົນຈົບເພື່ອໃຫ້ການປະຊຸມວິດີໂອທີ່ມີປະສິດທິພາບ, ການແບ່ງປັນໄຟລ໌ ແລະການໂທແບບສຽງ. + +ຄຸນສົມບັດຂອງລະບົບລວມມີ: +- ເຄື່ອງມືການສື່ສານອອນໄລນ໌ແບບລຳ້ໜ້າ +- ຂໍ້ຄວາມມີການເຂົ້າລະຫັດຢ່າງເຕັມສ່ວນເພື່ອໃຫ້ການສື່ສານຂອງອົງກອນມີຄວາມປອດໄພ, ເຖິງແມ່ນວ່າຈະເຮັດວຽກຈາກທາງໄກ +- ລະບົບແຊັດແບບແບ່ງກະຈາຍການຄຸ້ມຄອງໂດຍນຳໃຊ້ນະວັດຕະກຳເປີດຂອງລະບົບ Matrix +- ການແບ່ງປັນໄຟລ໌ຢ່າງປອດໄພກັບຂໍ້ມູນທີ່ເຂົ້າລະຫັດໄວ້ໃນຂະນະທີ່ຈັດການໂຄງການ +- ວິດີໂອສົນທະນາກັບ Voice over IP ແລະການແບ່ງປັນຫນ້າຈໍ +- ການເຊື່ອມໂຍງງ່າຍດາຍກັບເຄື່ອງມືການຮ່ວມມືອອນໄລນ໌ຂອງທ່ານ, ເຄື່ອງມືການຄຸ້ມຄອງໂຄງການ, ການບໍລິການ VoIP ແລະຂໍ້ຄວາມອື່ນໆຂອງທີມ + +ລະບົບນີ້ແມ່ນແຕກຕ່າງກັນຢ່າງສິ້ນເຊີງຈາກແອັບຯສົ່ງຂໍ້ຄວາມ ແລະແອັບສົ່ງເສີມການຮ່ວມມືອື່ນໆ. ມັນດໍາເນີນການຢູ່ໃນ Matrix, ເຄືອຂ່າຍເປີດສໍາລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພແລະການສື່ສານແບບກະຈາຍການຄຸ້ມຄອງ. ທ່ານສາມາດຕັ້ງສະຖານີດ້ວຍຕົນເອງເພື່ອໃຫ້ຜູ້ໃຊ້ເປັນເຈົ້າຂອງສູງສຸດແລະຄຸ້ມຄອງຂໍ້ມູນແລະຂໍ້ຄວາມຂອງຕົນເອງ. + +ຄວາມເປັນສ່ວນຕົວ ແລະຂໍ້ຄວາມເຂົ້າລະຫັດ +ລະບົບຈະປົກປ້ອງທ່ານຈາກການໂຄສະນາທີ່ບໍ່ຕ້ອງການ, ການຂຸດຄົ້ນຂໍ້ມູນແລະການສ້າງເງື່ອນໄຂປິດລ້ອມ. ລະບົບຍັງຮັບປະກັນຂໍ້ມູນທັງຫມົດຂອງທ່ານ, ການໂທວິດີໂອແບບຫນຶ່ງຕໍ່ຫນຶ່ງແລະການສື່ສານສຽງໂດຍຜ່ານການເຂົ້າລະຫັດແບບຕົ້ນທາງຫາປາຍທາງ ແລະການກວດສອບອຸປະກອນກ່ອນເຊື່ອມຕໍ່. + +ລະບົບເຮົາໃຫ້ທ່ານຄວບຄຸມຄວາມເປັນສ່ວນຕົວຂອງທ່ານໃນຂະນະທີ່ອະນຸຍາດໃຫ້ທ່ານຕິດຕໍ່ສື່ສານຢ່າງປອດໄພກັບທຸກຄົນໃນເຄືອຂ່າຍ Matrix, ຫຼືເຄື່ອງມືທີ່ທ່ານຄຸ້ນເຄີຍເຊັ່ນ Whatsapp, Slack, Facebook. + +ທ່ານສາມາດສ້າງສະຖານີຂອງຕົນເອງ +ເພື່ອໃຫ້ສາມາດຄວບຄຸມຂໍ້ມູນແລະການສົນທະນາທີ່ລະອຽດອ່ອນຂອງທ່ານຫຼາຍຂຶ້ນ, ທ່ານສາມາດຕິດຕັ້ງສະຖານີສື່ສານຂອງຕົນເອງຫຼືທ່ານສາມາດເລືອກຕິດຕັ້ງໄວ້ກັບເຄືອຂ່າຍ Matrix - ມາດຕະຖານສໍາລັບລະບົບເປີດ, ການສື່ສານແບບກະຈາຍການຄຸ້ມຄອງ. ລະບົບພວກເຮົາໃຫ້ທ່ານມີຄວາມເປັນສ່ວນຕົວ, ໄດ້ຕາມມາດຕະຖານຄວາມປອດໄພແລະມີຄວາມຍືດຫຍຸ່ນໃນການເຊື່ອມໂຍງ. + +ເປັນເຈົ້າຂອງຂໍ້ມູນຂອງທ່ານເອງ +ທ່ານສາມາດເລືອກໄດ້ວ່າຈະເກັບຂໍ້ມູນ ແລະຂໍ້ຄວາມຂອງທ່ານຢູ່ໃສ. ໂດຍບໍ່ມີຄວາມສ່ຽງຕໍ່ການຂຸດຄົ້ນຂໍ້ມູນຫຼືການເຂົ້າເຖິງຈາກພາກສ່ວນທີສາມ. + +ລະບົບພວກເຮົາເຮັດໃຫ້ທ່ານສາມາດຄວບຄຸມດ້ວຍວິທີຕ່າງໆ: +1. ຮັບບັນຊີຟຣີໃນເຊີບເວີສາທາລະນະ matrix.org ທີ່ໂຮສໂດຍຜູ້ພັດທະນາ Matrix, ຫຼືເລືອກຈາກເຄື່ອງແມ່ຂ່າຍສາທາລະນະຫຼາຍພັນເຊີບເວີທີ່ໂຮສໂດຍອາສາສະໝັກ. +2. ຈັດການບັນຊີຂອງທ່ານເອງໂດຍການແລ່ນເຊີບເວີໃນໂຄງລ່າງພື້ນຖານ IT ຂອງທ່ານເອງ +3. ລົງທະບຽນສໍາລັບບັນຊີຢູ່ໃນເຄື່ອງແມ່ຂ່າຍທີ່ກໍາຫນົດເອງໂດຍພຽງແຕ່ສະຫມັກກັບ Element Matrix Services hosting platform + +ສົ່ງຂໍ້ຄວາມ ແລະການຮ່ວມມືກັບລະບົບອື່ນໆທີ່ທ່ານຄຸ້ນເຄີຍ +ທ່ານສາມາດສົນທະນາກັບທຸກຄົນໃນເຄືອຂ່າຍ Matrix, ບໍ່ວ່າເຂົາເຈົ້າກໍາລັງໃຊ້ Element, ແອັບ Matrix ອື່ນ ຫຼືເຖິງແມ່ນວ່າເຂົາເຈົ້າກຳລັງໃຊ້ແອັບສົ່ງຂໍ້ຄວາມອື່ນຢູ່. + +ປອດໄພທີ່ສຸດ +ການເຂົ້າລະຫັດຈາກຕົ້ນທາງຫາປາຍທາງ (ມີພຽງແຕ່ຜູ້ທີ່ຢູ່ໃນການສົນທະນາສາມາດຖອດລະຫັດຂໍ້ຄວາມໄດ້), ແລະການຢັ້ງຢືນອຸປະກອນໂດຍການລົງນາມຂ້າມອຸປະກອນ. + +ການສື່ສານແລະການເຊື່ອມໂຍງທີ່ຄົບຖ້ວນສົມບູນ +ການສົ່ງຂໍ້ຄວາມ, ການໂທສຽງ ແລະວິດີໂອ, ການແບ່ງປັນໄຟລ໌, ການແບ່ງປັນໜ້າຈໍ ແລະການເຊື່ອມໂຍງທັງໝົດ, ບັອດ ແລະວິດເຈັດ. ສ້າງຫ້ອງ, ຊຸມຊົນ, ຕິດຕໍ່ພົວພັນແລະເຮັດສິ່ງຕ່າງໆ. + +ສືບຕໍ່ຈາກການສົນທະນາລ້າສຸດ +ທ່ານສາມາດຕິດຕໍ່ພົວພັນໄດ້ທຸກບ່ອນທີ່ຕ້ອງການ ເຊິ່ງປະຫວັດການສົນທະນາຈະຖືກອັບເດດໄປຫາບັນດາອຸປະກອນຕ່າງໆຂອງທ່ານ ລວມເຖິງໃນເວັບໄຊຕ໌ທີ່ https://app.element.io + +ເຕັກໂນໂລຊີໂອເພິນຊອດ +Element ເປັນໂຄງການເຕັກໂນໂລຊີໂອເພິນຊອດ, ເປັນເຈົ້າພາບໂດຍ GitHub. ກະລຸນາລາຍງານຂໍ້ບົກພ່ອງ ແລະ/ຫຼື ປະກອບສ່ວນເຂົ້າໃນການພັດທະນາຂອງລະບົບຢູ່ https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/lo/short_description.txt b/fastlane/metadata/android/lo/short_description.txt new file mode 100644 index 0000000000..625b078915 --- /dev/null +++ b/fastlane/metadata/android/lo/short_description.txt @@ -0,0 +1 @@ +ສົ່ງຂໍ້ຄວາມກຸ່ມ - ສົ່ງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ, ສົນທະນາກຸ່ມ ແລະໂທວິດີໂອ diff --git a/fastlane/metadata/android/lo/title.txt b/fastlane/metadata/android/lo/title.txt new file mode 100644 index 0000000000..a9054d803b --- /dev/null +++ b/fastlane/metadata/android/lo/title.txt @@ -0,0 +1 @@ +ລະບົບສື່ສານທີ່ມີຄວາມປອດໄພສູງ diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104100.txt b/fastlane/metadata/android/pt-BR/changelogs/40104100.txt new file mode 100644 index 0000000000..eb746aca2b --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Rolar em mensagem de voz. Vários consertos de bugs e melhorias de estabilidade. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104110.txt b/fastlane/metadata/android/pt-BR/changelogs/40104110.txt new file mode 100644 index 0000000000..6e11e92579 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Vários consertos de bugs e melhorias de estabilidade. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104100.txt b/fastlane/metadata/android/sk/changelogs/40104100.txt new file mode 100644 index 0000000000..b96a539364 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Posúvanie v hlasovej správe. Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104110.txt b/fastlane/metadata/android/sk/changelogs/40104110.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104100.txt b/fastlane/metadata/android/sq/changelogs/40104100.txt new file mode 100644 index 0000000000..c45c2b9a33 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Rrëshqitje në mesazh zanor. Ndreqje të metash të ndryshme dhe përmirësime qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104110.txt b/fastlane/metadata/android/sq/changelogs/40104110.txt new file mode 100644 index 0000000000..df9b377adb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash të ndryshme dhe përmirësime qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104100.txt b/fastlane/metadata/android/sv-SE/changelogs/40104100.txt new file mode 100644 index 0000000000..a1585aacf5 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i denna version: Scrolla i röstmeddelanden. Diverse buggfixar och förbättrad stabilitet. +Fullständig lista över ändringar: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104110.txt b/fastlane/metadata/android/sv-SE/changelogs/40104110.txt new file mode 100644 index 0000000000..92cce13669 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i denna version: Diverse buggfixar och förbättrad stabilitet. +Fullständig lista över ändringar: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104100.txt b/fastlane/metadata/android/uk/changelogs/40104100.txt new file mode 100644 index 0000000000..93c9fe4b70 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Прокручування в голосових повідомленнях. Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104110.txt b/fastlane/metadata/android/uk/changelogs/40104110.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104100.txt b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt new file mode 100644 index 0000000000..c78ed7dd2d --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:捲動音訊訊息。多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104110.txt b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104110.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle index ad3a948808..d3afd8d29b 100644 --- a/library/core-utils/build.gradle +++ b/library/core-utils/build.gradle @@ -44,7 +44,7 @@ android { kotlinOptions { jvmTarget = "11" freeCompilerArgs += [ - "-Xopt-in=kotlin.RequiresOptIn" + "-opt-in=kotlin.RequiresOptIn" ] } } @@ -52,4 +52,4 @@ android { dependencies { implementation libs.androidx.appCompat implementation libs.jetbrains.coroutinesAndroid -} \ No newline at end of file +} diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index 0cad8ac171..2110747feb 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -6,8 +6,14 @@ apply plugin: 'com.jakewharton.butterknife' buildscript { repositories { - google() - mavenCentral() + // Do not use `google()`, it prevents Dependabot from working properly + maven { + url 'https://maven.google.com' + } + // Do not use `mavenCentral()`, it prevents Dependabot from working properly + maven { + url 'https://repo1.maven.org/maven2' + } } dependencies { classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 2aab8510ce..c840b9a6e9 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -7,7 +7,10 @@ apply plugin: "org.jetbrains.dokka" buildscript { repositories { - mavenCentral() + // Do not use `mavenCentral()`, it prevents Dependabot from working properly + maven { + url 'https://repo1.maven.org/maven2' + } } dependencies { classpath "io.realm:realm-gradle-plugin:10.9.0" @@ -53,7 +56,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.14\"" + buildConfigField "String", "SDK_VERSION", "\"1.4.16\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -98,6 +101,9 @@ android { freeCompilerArgs += [ // Disabled for now, there are too many errors. Could be handled in another dedicated PR // '-Xexplicit-api=strict', // or warning + "-opt-in=kotlin.RequiresOptIn", + // Opt in for kotlinx.coroutines.FlowPreview + "-opt-in=kotlinx.coroutines.FlowPreview", ] } @@ -175,7 +181,7 @@ dependencies { implementation libs.arrow.instances // olm lib is now hosted in MavenCentral - implementation 'org.matrix.android:olm-sdk:3.2.10' + implementation 'org.matrix.android:olm-sdk:3.2.11' // DI implementation libs.dagger.dagger @@ -192,7 +198,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.47' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.7.3' diff --git a/matrix-sdk-android/docs/modules.md b/matrix-sdk-android/docs/modules.md index edf5af64d0..b19bc73534 100644 --- a/matrix-sdk-android/docs/modules.md +++ b/matrix-sdk-android/docs/modules.md @@ -1,5 +1,8 @@ # Module matrix-sdk-android + +**Note**: You are viewing the nightly documentation of the Android Matrix SDK library. The documentation of the released library can be found here: [https://matrix-org.github.io/matrix-android-sdk2/](https://matrix-org.github.io/matrix-android-sdk2/) + ## Welcome to the matrix-sdk-android documentation! This pages list the complete API that this SDK is exposing to a client application. diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt index b11a538949..8717b3f7a8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt @@ -37,7 +37,10 @@ class PermalinkParserTest { Assert.assertTrue("Should be parsed as email invite but was ${parsedLink::class.java}", parsedLink is PermalinkData.RoomEmailInviteLink) parsedLink as PermalinkData.RoomEmailInviteLink Assert.assertEquals("!MRBNLPtFnMAazZVPMO:matrix.org", parsedLink.roomId) - Assert.assertEquals("XmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe", parsedLink.token) + Assert.assertEquals( + "XmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe", + parsedLink.token + ) Assert.assertEquals("vector.im", parsedLink.identityServer) Assert.assertEquals("Team2", parsedLink.roomName) Assert.assertEquals("hiphop5", parsedLink.inviterName) @@ -45,7 +48,8 @@ class PermalinkParserTest { @Test fun testParseLinkWIthEvent() { - val rawInvite = "https://matrix.to/#/!OGEhHVWSdvArJzumhm:matrix.org/\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc?via=matrix.org&via=libera.chat&via=matrix.example.io" + val rawInvite = + "https://matrix.to/#/!OGEhHVWSdvArJzumhm:matrix.org/\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc?via=matrix.org&via=libera.chat&via=matrix.example.io" val parsedLink = PermalinkParser.parse(rawInvite) Assert.assertTrue("Should be parsed as room link", parsedLink is PermalinkData.RoomLink) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt index 3e3af10799..a44cd6c80f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt @@ -21,5 +21,7 @@ import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import java.util.concurrent.Executors -internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, - Executors.newSingleThreadExecutor().asCoroutineDispatcher()) +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers( + Main, Main, Main, Main, + Executors.newSingleThreadExecutor().asCoroutineDispatcher() +) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 52dbfc7155..d2dfe4d945 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -67,9 +67,11 @@ class DeactivateAccountTest : InstrumentedTest { val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) // Test the error - assertTrue(throwable is Failure.ServerError && - throwable.error.code == MatrixError.M_USER_DEACTIVATED && - throwable.error.message == "This account has been deactivated") + assertTrue( + throwable is Failure.ServerError && + throwable.error.code == MatrixError.M_USER_DEACTIVATED && + throwable.error.message == "This account has been deactivated" + ) // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) val hs = commonTestHelper.createHomeServerConfig() @@ -95,8 +97,10 @@ class DeactivateAccountTest : InstrumentedTest { // Test the error accountCreationError.let { - assertTrue(it is Failure.ServerError && - it.error.code == MatrixError.M_USER_IN_USE) + assertTrue( + it is Failure.ServerError && + it.error.code == MatrixError.M_USER_IN_USE + ) } // No need to close the session, it has been deactivated diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 54d02d226e..4b9e605cd0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -57,7 +57,8 @@ import java.util.concurrent.TimeUnit class CommonTestHelper(context: Context) { internal val matrix: TestMatrix - val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private var accountNumber = 0 fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor @@ -167,7 +168,8 @@ class CommonTestHelper(context: Context) { if (rootThreadEventId != null) { room.relationService().replyInThread( rootThreadEventId = rootThreadEventId, - replyInThreadText = formattedMessage) + replyInThreadText = formattedMessage + ) } else { room.sendService().sendTextMessage(formattedMessage) } @@ -237,7 +239,7 @@ class CommonTestHelper(context: Context) { password: String, testParams: SessionTestParams): Session { val session = createAccountAndSync( - userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(), + userNamePrefix + "_" + accountNumber++ + "_" + UUID.randomUUID(), password, testParams ) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 058b1f7933..dfb4863d5b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.common import android.os.SystemClock import android.util.Log import androidx.lifecycle.Observer +import org.amshove.kluent.fail import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.api.extensions.orFalse 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.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod @@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.awaitCallback +import org.matrix.android.sdk.api.util.toBase64NoPadding import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -291,7 +306,61 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { ) ) } - }, it) + }, it + ) + } + } + + /** + * Initialize cross-signing, set up megolm backup and save all in 4S + */ + fun bootstrapSecurity(session: Session) { + initializeCrossSigning(session) + val ssssService = session.sharedSecretStorageService() + testHelper.runBlockingTest { + val keyInfo = ssssService.generateKey( + UUID.randomUUID().toString(), + null, + "ssss_key", + EmptyKeySigner() + ) + ssssService.setDefaultKey(keyInfo.keyId) + + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + // set up megolm backup + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> + ssssService.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + secret, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + } } } @@ -299,13 +368,47 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) - val requestID = UUID.randomUUID().toString() val aliceVerificationService = alice.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService() + val localId = UUID.randomUUID().toString() + aliceVerificationService.requestKeyVerificationInDMs( + localId = localId, + methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + otherUserId = bob.myUserId, + roomId = roomId + ).transactionId + + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } != null + } + } + val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } + bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!) + + var requestID: String? = null + // wait for it to be readied + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) + .firstOrNull { it.localId == localId } + if (outgoingRequest?.isReady == true) { + requestID = outgoingRequest.transactionId!! + true + } else { + false + } + } + } + aliceVerificationService.beginKeyVerificationInDMs( VerificationMethod.SAS, - requestID, + requestID!!, roomId, bob.myUserId, bob.sessionParams.credentials.deviceId!!) @@ -314,23 +417,9 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { var alicePovTx: OutgoingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null - // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") - if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() - true - } else { - false - } - } - } - - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") alicePovTx?.state == VerificationTxState.ShortCodeReady } @@ -338,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") if (bobPovTx?.state == VerificationTxState.OnStarted) { bobPovTx?.performAccept() @@ -390,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { return CryptoTestData(roomId, sessions) } + + fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { + sentEventIds.forEachIndexed { index, sentEventId -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } + } catch (error: MXCryptoError) { + // nop + } + } + Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel()?.body + } + } + } + } + + fun ensureCannotDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { + sentEventIds.forEach { sentEventId -> + val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + if (expectedError == null) { + assertNotNull(errorType) + } else { + assertEquals("Unexpected reason", expectedError, errorType) + } + } + } + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt index dc58339498..525e168cf1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -29,15 +29,17 @@ import org.matrix.android.sdk.internal.raw.RawModule import org.matrix.android.sdk.internal.settings.SettingsModule import org.matrix.android.sdk.internal.util.system.SystemModule -@Component(modules = [ - TestModule::class, - MatrixModule::class, - NetworkModule::class, - AuthModule::class, - RawModule::class, - SettingsModule::class, - SystemModule::class -]) +@Component( + modules = [ + TestModule::class, + MatrixModule::class, + NetworkModule::class, + AuthModule::class, + RawModule::class, + SettingsModule::class, + SystemModule::class + ] +) @MatrixScope internal interface TestMatrixComponent : MatrixComponent { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index 732f4f7dce..f5f585a1e0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -29,8 +29,10 @@ import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.util.time.DefaultClock import java.io.ByteArrayOutputStream import java.io.InputStream +import java.util.UUID /** * Unit tests AttachmentEncryptionTest. @@ -48,13 +50,18 @@ class AttachmentEncryptionTest { inputStream = if (inputAsByteArray.isEmpty()) { inputAsByteArray.inputStream() } else { - val memoryFile = MemoryFile("file" + System.currentTimeMillis(), inputAsByteArray.size) + val memoryFile = MemoryFile("file_" + UUID.randomUUID(), inputAsByteArray.size) memoryFile.outputStream.write(inputAsByteArray) memoryFile.inputStream } val decryptedStream = ByteArrayOutputStream() - val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream) + val result = MXEncryptedAttachments.decryptAttachment( + attachmentStream = inputStream, + elementToDecrypt = encryptedFileInfo.toElementToDecrypt()!!, + outputStream = decryptedStream, + clock = DefaultClock() + ) assert(result) @@ -117,9 +124,13 @@ class AttachmentEncryptionTest { url = "dummyUrl" ) - assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", - checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", - encryptedFileInfo)) + assertEquals( + "YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption( + "zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", + encryptedFileInfo + ) + ) } @Test @@ -138,8 +149,12 @@ class AttachmentEncryptionTest { url = "dummyUrl" ) - assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", - checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", - encryptedFileInfo)) + assertNotEquals( + "YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption( + "tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", + encryptedFileInfo + ) + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt index c717c8e33f..ba1afd4758 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.time.DefaultClock import kotlin.random.Random internal class CryptoStoreHelper { @@ -34,7 +35,8 @@ internal class CryptoStoreHelper { .build(), crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), userId = "userId_" + Random.nextInt(), - deviceId = "deviceId_sample" + deviceId = "deviceId_sample", + clock = DefaultClock(), ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt index f43c425cc9..e823aa39a1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -27,6 +27,7 @@ import org.junit.runner.RunWith import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmManager import org.matrix.olm.OlmSession @@ -37,6 +38,7 @@ private const val DUMMY_DEVICE_KEY = "DeviceKey" class CryptoStoreTest : InstrumentedTest { private val cryptoStoreHelper = CryptoStoreHelper() + private val clock = DefaultClock() @Before fun setup() { @@ -74,9 +76,11 @@ class CryptoStoreTest : InstrumentedTest { } val olmSession1 = OlmSession().apply { - initOutboundSession(olmAccount1, + initOutboundSession( + olmAccount1, olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() + ) } val sessionId1 = olmSession1.sessionIdentifier() @@ -91,9 +95,11 @@ class CryptoStoreTest : InstrumentedTest { } val olmSession2 = OlmSession().apply { - initOutboundSession(olmAccount2, + initOutboundSession( + olmAccount2, olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() + ) } val sessionId2 = olmSession2.sessionIdentifier() @@ -106,7 +112,7 @@ class CryptoStoreTest : InstrumentedTest { // Note: we cannot be sure what will be the result of getLastUsedSessionId() here - olmSessionWrapper2.onMessageReceived() + olmSessionWrapper2.onMessageReceived(clock.epochMillis()) cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) // sessionId2 is returned now @@ -114,7 +120,7 @@ class CryptoStoreTest : InstrumentedTest { Thread.sleep(2) - olmSessionWrapper1.onMessageReceived() + olmSessionWrapper1.onMessageReceived(clock.epochMillis()) cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) // sessionId1 is returned now diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index 88d99f12e0..ebe4c5ff6f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest 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.RequestResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary @@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestMatrixCallback +import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class E2eeSanityTests : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testSendingE2EEMessages() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val e2eRoomID = cryptoTestData.roomId val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + // we want to disable key gossiping to just check initial sending of keys + aliceSession.cryptoService().enableKeyGossiping(false) + cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) // add some more users and invite them val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") .map { - testHelper.createAccount(it, SessionTestParams(true)) + testHelper.createAccount(it, SessionTestParams(true)).also { + it.cryptoService().enableKeyGossiping(false) + } } Log.v("#E2E TEST", "All accounts created") @@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest { // All user should accept invite otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) + waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") } // check that alice see them as joined (not really necessary?) - ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID) + ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID) Log.v("#E2E TEST", "All users have joined the room") Log.v("#E2E TEST", "Alice is sending the message") val text = "This is my message" - val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text) + val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() Assert.assertTrue("Message should be sent", sentEventId != null) @@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest { otherAccounts.forEach { otherSession -> testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } @@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - waitForAndAcceptInviteInRoom(it, e2eRoomID) + waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) } - ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) + ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) // wait a bit testHelper.runBlockingTest { @@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends a new message") val secondMessage = "2 This is my message" - val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage) + val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) // new members should be able to decrypt it newAccount.forEach { otherSession -> @@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } + @Test + fun testKeyGossipingIsEnabledByDefault() { + val testHelper = CommonTestHelper(context()) + val session = testHelper.createAccount("alice", SessionTestParams(true)) + Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled()) + testHelper.signOutAndClose(session) + } + /** * Quick test for basic key backup * 1. Create e2e between Alice and Bob @@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testBasicBackupImport() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! @@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest { val sentEventIds = mutableListOf() val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { sentEventIds.add(it) } testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } // we want more so let's discard the session @@ -289,28 +314,31 @@ class E2eeSanityTests : InstrumentedTest { } } // after initial sync events are not decrypted, so we have to try manually - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // Let's now import keys from backup - newBobSession.cryptoService().keysBackupService().let { keysBackupService -> + newBobSession.cryptoService().keysBackupService().let { kbs -> val keyVersionResult = testHelper.doSync { - keysBackupService.getVersion(version.version, it) + kbs.getVersion(version.version, it) } val importedResult = testHelper.doSync { - keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!, + kbs.restoreKeyBackupWithPassword( + keyVersionResult!!, keyBackupPassword, null, null, - null, it) + null, + it + ) } assertEquals(3, importedResult.totalNumberOfKeys) } // ensure bob can now decrypt - ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) testHelper.signOutAndClose(newBobSession) } @@ -321,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testSimpleGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! @@ -328,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest { val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - cryptoTestHelper.initializeCrossSigning(bobSession) - // let's send a few message to bob val sentEventIds = mutableListOf() val messagesText = listOf("1. Hello", "2. Bob") Log.v("#E2E TEST", "Alice sends some messages") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { sentEventIds.add(it) } testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } // Ensure bob can decrypt - ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID) + ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID) // Let's now add a new bob session // Create a new session for bob @@ -361,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest { // check that new bob can't currently decrypt Log.v("#E2E TEST", "check that new bob can't currently decrypt") - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) +// newBobSession.cryptoService().getOutgoingRoomKeyRequests() +// .firstOrNull { +// it.sessionId == +// } // Try to request sentEventIds.forEach { sentEventId -> @@ -370,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest { } // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } + // we need to wait a couple of syncs to let sharing occurs +// testHelper.waitFewSyncs(newBobSession, 6) // Ensure that new bob still can't decrypt (keys must have been withheld) - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD) + sentEventIds.forEach { sentEventId -> + val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! + .getTimelineEvent(sentEventId)!! + .root.content.toModel()!!.sessionId + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() + .first { + it.sessionId == megolmSessionId && + it.roomId == e2eRoomID + } + .results.also { + Log.w("##TEST", "result list is $it") + } + .firstOrNull { it.userId == aliceSession.myUserId } + ?.result + aliceReply != null && + aliceReply is RequestResult.Failure && + WithHeldCode.UNAUTHORISED == aliceReply.code + } + } + } + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) // Now mark new bob session as verified @@ -388,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest { newBobSession.cryptoService().reRequestRoomKeyForEvent(event) } - // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } - - ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) cryptoTestData.cleanUp(testHelper) testHelper.signOutAndClose(newBobSession) @@ -404,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testForwardBetterKey() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSessionWithBetterKey = cryptoTestData.secondSession!! @@ -411,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest { val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey) - // let's send a few message to bob var firstEventId: String val firstMessage = "1. Hello" Log.v("#E2E TEST", "Alice sends some messages") firstMessage.let { text -> - firstEventId = sendMessageInRoom(aliceRoomPOV, text)!! + firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } // Ensure bob can decrypt - ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) + ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) // Let's add a new unverified session from bob val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) // check that new bob can't currently decrypt Log.v("#E2E TEST", "check that new bob can't currently decrypt") - ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null) // Now let alice send a new message. this time the new bob session will be able to decrypt var secondEventId: String @@ -447,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends some messages") secondMessage.let { text -> - secondEventId = sendMessageInRoom(aliceRoomPOV, text)!! + secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } @@ -473,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest { try { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType) + } catch (_: MXCryptoError) { } } @@ -497,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest { .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) // now let new session request - newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root) + newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root) - // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } + // We need to wait for the key request to be sent out and then a reply to be received // old session should have shared the key at earliest known index now // we should be able to decrypt both - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt first event now $error") - } - } - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt event $error") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val canDecryptFirst = try { + testHelper.runBlockingTest { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + } + true + } catch (error: MXCryptoError) { + false + } + val canDecryptSecond = try { + testHelper.runBlockingTest { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } + true + } catch (error: MXCryptoError) { + false + } + canDecryptFirst && canDecryptSecond } } - cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSessionWithBetterKey) testHelper.signOutAndClose(newBobSession) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { + private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { aliceRoomPOV.sendService().sendTextMessage(text) var sentEventId: String? = null testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) timeline.start() - testHelper.retryPeriodicallyWithLatch(latch) { val decryptedMsg = timeline.getSnapshot() .filter { it.root.getClearType() == EventType.MESSAGE } @@ -550,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest { return sentEventId } - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + /** + * Test that if a better key is forwared (lower index, it is then used) + */ + @Test + fun testSelfInteractiveVerificationAndGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) + cryptoTestHelper.bootstrapSecurity(aliceSession) + + // now let's create a new login from alice + + val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + val oldCompleteLatch = CountDownLatch(1) + lateinit var oldCode: String + aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + val readyInfo = pr.readyInfo + if (readyInfo != null) { + aliceSession.cryptoService().verificationService().beginKeyVerification( + VerificationMethod.SAS, + aliceSession.myUserId, + readyInfo.fromDevice, + readyInfo.transactionId + + ) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "exitsingPov: $tx") + val sasTx = tx as OutgoingSasVerificationTransaction + when (sasTx.uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + // for the test we just accept? + oldCode = sasTx.getDecimalCodeRepresentation() + sasTx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + // we can release this latch? + oldCompleteLatch.countDown() + } + else -> Unit + } + } + }) + + val newCompleteLatch = CountDownLatch(1) + lateinit var newCode: String + aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // let's ready + aliceNewSession.cryptoService().verificationService().readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + pr.transactionId!! + ) + } + + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "newPov: $tx") + + val sasTx = tx as IncomingSasVerificationTransaction + when (sasTx.uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + // no need to accept as there was a request first it will auto accept + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + sasTx.userHasVerifiedShortCode() + newCode = sasTx.getDecimalCodeRepresentation() + matchOnce = false + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + newCompleteLatch.countDown() + } + else -> Unit + } + } + }) + + // initiate self verification + aliceSession.cryptoService().verificationService().requestKeyVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceNewSession.myUserId, + listOf(aliceNewSession.sessionParams.deviceId!!) + ) + testHelper.await(oldCompleteLatch) + testHelper.await(newCompleteLatch) + assertEquals("Decimal code should have matched", oldCode, newCode) + + // Assert that devices are verified + val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) + + Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) + Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) + + // wait for secret gossiping to happen + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() + } + } + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null + } + } + + assertEquals( + "MSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master + ) + assertEquals( + "USK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user + ) + + assertEquals( + "SSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned + ) + + // Let's check that we have the megolm backup key + assertEquals( + "Megolm key should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey + ) + assertEquals( + "Megolm version should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version + ) + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(aliceNewSession) + } + + private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { otherAccounts.map { @@ -562,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { + private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { val roomSummary = otherSession.getRoomSummary(e2eRoomID) @@ -574,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest { } } - testHelper.runBlockingTest(60_000) { + // not sure why it's taking so long :/ + testHelper.runBlockingTest(90_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") try { otherSession.roomService().joinRoom(e2eRoomID) @@ -592,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) { - sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "").let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } - } catch (error: MXCryptoError) { - // nop - } - } - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel()?.body - } - } - } - } - - private fun ensureIsDecrypted(sentEventIds: List, session: Session, e2eRoomID: String) { + private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> sentEventIds.forEach { sentEventId -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE - } - } - } - } - - private fun ensureCannotDecrypt(sentEventIds: List, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) { - sentEventIds.forEach { sentEventId -> - val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(event, "") - fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - if (expectedError == null) { - Assert.assertNotNull(errorType) - } else { - assertEquals(expectedError, errorType, "Message expected to be UISI") - } + val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt index 17664c78aa..c2d8f4fb35 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt @@ -93,9 +93,11 @@ class ExportEncryptionTest { fail("## checkExportDecrypt1() failed : " + e.message) } - assertEquals("## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -111,9 +113,11 @@ class ExportEncryptionTest { fail("## checkExportDecrypt2() failed : " + e.message) } - assertEquals("## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -129,9 +133,11 @@ class ExportEncryptionTest { fail("## checkExportDecrypt3() failed : " + e.message) } - assertEquals("## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -147,9 +153,11 @@ class ExportEncryptionTest { fail("## checkExportEncrypt1() failed : " + e.message) } - assertEquals("## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -165,9 +173,11 @@ class ExportEncryptionTest { fail("## checkExportEncrypt2() failed : " + e.message) } - assertEquals("## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -183,9 +193,11 @@ class ExportEncryptionTest { fail("## checkExportEncrypt3() failed : " + e.message) } - assertEquals("## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } @Test @@ -201,8 +213,10 @@ class ExportEncryptionTest { fail("## checkExportEncrypt4() failed : " + e.message) } - assertEquals("## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString", + assertEquals( + "## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString", expectedString, - decodedString) + decodedString + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index 8a1edec5e3..93aa78a305 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent @@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest { // clear any outbound session aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - val preShareCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } + val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) Log.d("#Test", "Room Key Received from alice $preShareCount") @@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val newGossipCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } - newGossipCount > preShareCount + val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() + newKeysCount > preShareCount } } - val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } + val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier() - val content = latest?.getClearContent().toModel() - assertNotNull("Bob should have received and decrypted a room key event from alice", content) - assertEquals("Wrong room", e2eRoomID, content!!.roomId) - val megolmSessionId = content.sessionId!! + val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!! + val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!) + assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) + assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) + + val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier() + + assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index de4a928dc3..0f3a4b4181 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -171,7 +171,10 @@ class UnwedgingTest : InstrumentedTest { // Let us wedge the session now. Set crypto state like after the first message Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") - aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!) + aliceCryptoStore.storeSession( + OlmSessionWrapper(deserializeFromRealm(oldSession)!!), + bobSession.cryptoService().getMyDevice().identityKey()!! + ) olmDevice.clearOlmSessionCache() Thread.sleep(6_000) @@ -218,7 +221,8 @@ class UnwedgingTest : InstrumentedTest { ) ) } - }, it) + }, it + ) } // Wait until we received back the key diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index 0f3ff7898f..a37626dc20 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -126,8 +126,16 @@ class XSigningTest : InstrumentedTest { assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + assertEquals( + "Bob keys from alice pov should match", + bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, + bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey + ) + assertEquals( + "Bob keys from alice pov should match", + bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, + bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey + ) assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 592d24fb69..2e4fd62822 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest -import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail +import org.amshove.kluent.internal.assertEquals import org.junit.Assert +import org.junit.Assert.assertNull import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class KeyShareTests : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test - @Ignore("This test will be ignored until it is fixed") fun test_DoNotSelfShareIfNotTrusted() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message val roomId = commonTestHelper.runBlockingTest { @@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest { assertNotNull(room) Thread.sleep(4_000) assertTrue(room?.roomCryptoService()?.isEncrypted() == true) - val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId - // Open a new sessionx + val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first() + val sentEventId = sentEvent.eventId + val sentEventText = sentEvent.getLastMessageContent()?.body - val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + // Open a new session + val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false)) + // block key requesting for now as decrypt will send requests (room summary is trying to decrypt) + aliceSession2.cryptoService().enableKeyGossiping(false) + commonTestHelper.syncSession(aliceSession2) + + Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") val roomSecondSessionPOV = aliceSession2.getRoom(roomId) @@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest { } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size) + // Try to request + aliceSession2.cryptoService().enableKeyGossiping(true) aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId @@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession2.cryptoService().getOutgoingRoomKeyRequests() - .filter { req -> - // filter out request that was known before - !outgoingRequestsBefore.any { req.requestId == it.requestId } - } .let { val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } outGoingRequestId = outgoing?.requestId @@ -141,17 +133,34 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS - aliceSession.cryptoService().getIncomingRoomKeyRequests().let { - Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") +// aliceSession.cryptoService().getIncomingRoomKeyRequests().let { +// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") +// Log.v("TEST", "=========================") +// it.forEach { keyRequest -> +// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") +// } +// Log.v("TEST", "=========================") +// } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> Log.v("TEST", "=========================") - it.forEach { keyRequest -> - Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}") - } + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") Log.v("TEST", "=========================") } - val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - incoming?.state == GossipingRequestState.REJECTED + val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val resultCode = (reply?.result as? RequestResult.Failure)?.code + resultCode == WithHeldCode.UNVERIFIED } } @@ -164,255 +173,309 @@ class KeyShareTests : InstrumentedTest { } // Mark the device as trusted - aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.deviceId ?: "") + aliceSession.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession2.sessionParams.deviceId ?: "" + ) // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.cryptoService().getIncomingRoomKeyRequests().let { - Log.v("TEST", "Incoming request Session 1") - Log.v("TEST", "=========================") - it.forEach { - Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}") - } - Log.v("TEST", "=========================") - - it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED } - } - } - } - - Thread.sleep(6_000) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let { - it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } - } - } - } - - try { - commonTestHelper.runBlockingTest { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } - } catch (failure: Throwable) { - fail("should have been able to decrypt") - } + cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: "")) commonTestHelper.signOutAndClose(aliceSession) commonTestHelper.signOutAndClose(aliceSession2) } + // See E2ESanityTest for a test regarding secret sharing + + /** + * Test that the sender of a message accepts to re-share to another user + * if the key was originally shared with him + */ @Test - @Ignore("This test will be ignored until it is fixed") - fun test_ShareSSSSSecret() { - val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + fun test_reShareIfWasIntendedToBeShared() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - commonTestHelper.doSync { - aliceSession1.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = aliceSession1.myUserId, - password = TestConstants.PASSWORD - ) - ) - } - }, it) - } + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! + val bobSession = testData.secondSession!! - // Also bootstrap keybackup on first session - val creationInfo = commonTestHelper.doSync { - aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) - } - val version = commonTestHelper.doSync { - aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() + val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! - val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true)) + // bob should be able to decrypt + cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: "")) - val aliceVerificationService1 = aliceSession1.cryptoService().verificationService() - val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() - - // force keys download - commonTestHelper.doSync> { - aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it) - } - commonTestHelper.doSync> { - aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it) - } - - var session1ShortCode: String? = null - var session2ShortCode: String? = null - - aliceVerificationService1.addListener(object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.OnStarted) { - (tx as IncomingSasVerificationTransaction).performAccept() - } - if (tx.state == VerificationTxState.ShortCodeReady) { - session1ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) - tx.userHasVerifiedShortCode() - } - } - } - }) - - aliceVerificationService2.addListener(object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.ShortCodeReady) { - session2ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) - tx.userHasVerifiedShortCode() - } - } - } - }) - - val txId = "m.testVerif12" - aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId - ?: "", txId) + // Let's try to request any how. + // As it was share previously alice should accept to reshare + bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true + val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + aliceReply != null && aliceReply.result is RequestResult.Success } } - - assertNotNull(session1ShortCode) - Log.d("#TEST", "session1ShortCode: $session1ShortCode") - assertNotNull(session2ShortCode) - Log.d("#TEST", "session2ShortCode: $session2ShortCode") - assertEquals(session1ShortCode, session2ShortCode) - - // SSK and USK private keys should have been shared - - commonTestHelper.waitWithLatch(60_000) { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}") - aliceSession2.cryptoService().crossSigningService().canCrossSign() - } - } - - // Test that key backup key has been shared to - commonTestHelper.waitWithLatch(60_000) { latch -> - val keysBackupService = aliceSession2.cryptoService().keysBackupService() - commonTestHelper.retryPeriodicallyWithLatch(latch) { - Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}") - keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey - } - } - - commonTestHelper.signOutAndClose(aliceSession1) - commonTestHelper.signOutAndClose(aliceSession2) } + /** + * Test that our own devices accept to reshare to unverified device if it was shared initialy + * if the key was originally shared with him + */ @Test - @Ignore("This test will be ignored until it is fixed") - fun test_ImproperKeyShareBug() { - val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + fun test_reShareToUnverifiedIfWasIntendedToBeShared() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - commonTestHelper.doSync { - aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, it) + val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) + val aliceSession = testData.firstSession + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! + + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + // we wait for alice first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null + } } + val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() + val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! - // Create an encrypted room and send a couple of messages - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom( - CreateRoomParams().apply { - visibility = RoomDirectoryVisibility.PRIVATE - enableEncryption() - } - ) + // Let's try to request any how. + // As it was share previously alice should accept to reshare + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success + } } - val roomAlicePov = aliceSession.getRoom(roomId) - assertNotNull(roomAlicePov) - Thread.sleep(1_000) - assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true) - val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId + } - // Create bob session + /** + * Tests that keys reshared with own verified session are done from the earliest known index + */ + @Test + fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - commonTestHelper.doSync { - bobSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, it) - } + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val roomFromBob = bobSession.getRoom(testData.roomId)!! - // Let alice invite bob - commonTestHelper.runBlockingTest { - roomAlicePov.membershipService().invite(bobSession.myUserId, null) - } + val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3) + val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!! - commonTestHelper.runBlockingTest { - bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList()) - } + // Let alice now add a new session + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false)) + aliceNewSession.cryptoService().enableKeyGossiping(false) + commonTestHelper.syncSession(aliceNewSession) - // we want to discard alice outbound session - aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId) - - // and now resend a new message to reset index to 0 - commonTestHelper.sendTextMessage(roomAlicePov, "After", 1) - - val roomRoomBobPov = aliceSession.getRoom(roomId) - val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId) - - var dRes = tryOrNull { - commonTestHelper.runBlockingTest { - bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") + // we wait bob first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } } - assert(dRes == null) + val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() + val newEventId = newEvent.eventId + val newEventText = newEvent.getLastMessageContent()!!.body - // Try to re-ask the keys + // alice should be able to decrypt the new one + cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText)) + // but not the first one! + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) - bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root) + // All should be using the same session id + sentEvents.forEach { + assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId) + } + assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId) - Thread.sleep(3_000) + // Request a first time, bob should reply with unauthorized and alice should reply with unverified + aliceNewSession.cryptoService().enableKeyGossiping(true) + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root) - // With the bug the first session would have improperly reshare that key :/ - dRes = tryOrNull { - commonTestHelper.runBlockingTest { - bobSession.cryptoService().decryptEvent(beforeJoin.root, "") + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + Log.v("TEST", "own device result is $result") + result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED } } - Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}") - assert(dRes?.clearEvent == null) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } + val result = bobDeviceReply?.result + Log.v("TEST", "bob device result is $result") + result != null && result is RequestResult.Success && result.chainIndex > 0 + } + } + + // it's a success but still can't decrypt first message + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) + + // Mark the new session as verified + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + + // Let's now try to request + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("TEST", "=========================") + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("TEST", "=========================") + } + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 + } + } + + // now the new session should be able to decrypt all! + cryptoTestHelper.ensureCanDecrypt( + sentEvents.map { it.eventId }, + aliceNewSession, + testData.roomId, + sentEvents.map { it.getLastMessageContent()!!.body } + ) + + // Additional test, can we check that bob replied successfully but with a ratcheted key + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 + } + } + + commonTestHelper.signOutAndClose(aliceNewSession) + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } + + /** + * Tests that we don't cancel a request to early on first forward if the index is not good enough + */ + @Test + fun test_dontCancelToEarly() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val roomFromBob = bobSession.getRoom(testData.roomId)!! + + val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3) + val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!! + + // Let alice now add a new session + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + // we wait bob first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null + } + } + + val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() + val newEventId = newEvent.eventId + val newEventText = newEvent.getLastMessageContent()!!.body + + // alice should be able to decrypt the new one + cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText)) + // but not the first one! + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) + + // All should be using the same session id + sentEvents.forEach { + assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId) + } + assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId) + + // Mark the new session as verified + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + + // /!\ Stop initial alice session syncing so that it can't reply + aliceSession.cryptoService().enableKeyGossiping(false) + aliceSession.stopSync() + + // Let's now try to request + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) + + // Should get a reply from bob and not from alice + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 + } + } + + val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + + assertNull("We should not have a reply from first session", outgoingReq!!.results.firstOrNull { it.fromDevice == aliceSession.sessionParams.deviceId }) + assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state) + + // let's wake up alice + aliceSession.cryptoService().enableKeyGossiping(true) + aliceSession.startSync(true) + + // We should now get a reply from first session + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 + } + } + + // It should be in sent then cancel + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state) + + commonTestHelper.signOutAndClose(aliceNewSession) + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index bad9fd0f68..cb31a2232f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -29,6 +28,7 @@ import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode @@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants @LargeTest class WithHeldTests : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldUnverifiedReason() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + // ============================= // ARRANGE // ============================= @@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest { val roomAlicePOV = aliceSession.getRoom(roomId)!! val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - // ============================= // ACT // ============================= @@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest { val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! + val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!! // ============================= // ASSERT // ============================= @@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest { val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } + // Let's see if the reply we got from bob first session is unverified + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() + .firstOrNull { it.sessionId == megolmSessionId } + ?.results + ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } + ?.result + ?.let { + it as? RequestResult.Failure + } + ?.code == WithHeldCode.UNVERIFIED + } + } // enable back sending to unverified aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) @@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest { val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } testHelper.signOutAndClose(aliceSession) @@ -139,21 +152,25 @@ class WithHeldTests : InstrumentedTest { } @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldNoOlm() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! val aliceInterceptor = testHelper.getTestInterceptor(aliceSession) // Simulate no OTK - aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( - "/keys/claim", - 200, - """ + aliceInterceptor!!.addRule( + MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ { "one_time_keys" : {} } """ - )) + ) + ) Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") val roomAlicePov = aliceSession.getRoom(testData.roomId)!! @@ -184,7 +201,10 @@ class WithHeldTests : InstrumentedTest { // Ensure that alice has marked the session to be shared with bob val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! - val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( + bobSession.myUserId, + bobSession.sessionParams.credentials.deviceId + ) Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) // Add a new device for bob @@ -202,7 +222,10 @@ class WithHeldTests : InstrumentedTest { } } - val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( + bobSecondSession.myUserId, + bobSecondSession.sessionParams.credentials.deviceId + ) Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) @@ -212,8 +235,10 @@ class WithHeldTests : InstrumentedTest { } @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldKeyRequest() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -259,5 +284,8 @@ class WithHeldTests : InstrumentedTest { wc?.code == WithHeldCode.UNAUTHORISED } } + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSecondSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt index d035fe5fde..9bf08f6fc0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt @@ -54,9 +54,11 @@ class KeysBackupPasswordTest : InstrumentedTest { assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) // Reverse operation - val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + val retrievedPrivateKey = retrievePrivateKeyWithPassword( + PASSWORD, generatePrivateKeyResult.salt, - generatePrivateKeyResult.iterations) + generatePrivateKeyResult.iterations + ) assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) assertArrayEquals(generatePrivateKeyResult.privateKey, retrievedPrivateKey) @@ -102,9 +104,11 @@ class KeysBackupPasswordTest : InstrumentedTest { assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) // Reverse operation, with bad password - val retrievedPrivateKey = retrievePrivateKeyWithPassword(BAD_PASSWORD, + val retrievedPrivateKey = retrievePrivateKeyWithPassword( + BAD_PASSWORD, generatePrivateKeyResult.salt, - generatePrivateKeyResult.iterations) + generatePrivateKeyResult.iterations + ) assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) @@ -122,9 +126,11 @@ class KeysBackupPasswordTest : InstrumentedTest { assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) // Reverse operation, with bad iteration - val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + val retrievedPrivateKey = retrievePrivateKeyWithPassword( + PASSWORD, generatePrivateKeyResult.salt, - 500_001) + 500_001 + ) assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) @@ -142,9 +148,11 @@ class KeysBackupPasswordTest : InstrumentedTest { assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) // Reverse operation, with bad iteration - val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + val retrievedPrivateKey = retrievePrivateKeyWithPassword( + PASSWORD, BAD_SALT, - generatePrivateKeyResult.iterations) + generatePrivateKeyResult.iterations + ) assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) @@ -168,7 +176,8 @@ class KeysBackupPasswordTest : InstrumentedTest { 116.toByte(), 224.toByte(), 229.toByte(), 224.toByte(), 9.toByte(), 3.toByte(), 178.toByte(), 162.toByte(), 120.toByte(), 23.toByte(), 108.toByte(), 218.toByte(), 22.toByte(), 61.toByte(), 241.toByte(), 200.toByte(), 235.toByte(), 173.toByte(), 236.toByte(), 100.toByte(), 115.toByte(), 247.toByte(), 33.toByte(), 132.toByte(), - 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte()) + 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte() + ) assertArrayEquals(privateKeyBytes, retrievedPrivateKey) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 3220f161fa..a7ddb6c553 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -272,10 +272,12 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg val sessionData = keysBackup - .decryptKeyBackupData(keyBackupData, + .decryptKeyBackupData( + keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, - decryption!!) + decryption!! + ) assertNotNull(sessionData) // - Compare the decrypted megolm key with the original one keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) @@ -297,7 +299,8 @@ class KeysBackupTest : InstrumentedTest { // - Restore the e2e backup from the homeserver val importRoomKeysResult = testHelper.doSync { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, null, null, @@ -680,7 +683,8 @@ class KeysBackupTest : InstrumentedTest { val steps = ArrayList() val importRoomKeysResult = testHelper.doSync { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, null, null, @@ -771,7 +775,8 @@ class KeysBackupTest : InstrumentedTest { // - Restore the e2e backup with the recovery key. val importRoomKeysResult = testHelper.doSync { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, null, null, @@ -1055,7 +1060,11 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup2.isEnabled) // - Validate the old device from the new one - aliceSession2.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession2.myUserId, oldDeviceId) + aliceSession2.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + aliceSession2.myUserId, + oldDeviceId + ) // -> Backup should automatically enable on the new device val latch4 = CountDownLatch(1) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index ac83cb8882..90e7fc1e45 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -88,10 +88,12 @@ internal class KeysBackupTestHelper( stateObserver.stopAndCheckStates(null) - return KeysBackupScenarioData(cryptoTestData, + return KeysBackupScenarioData( + cryptoTestData, aliceKeys, prepareKeysBackupDataResult, - aliceSession2) + aliceSession2 + ) } fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService, diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index a882f69013..c758050fc9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -207,14 +207,16 @@ class QuadSTests : InstrumentedTest { // Assert that can decrypt with both keys testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret("my.secret", + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! ) } testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret("my.secret", + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! ) @@ -245,13 +247,15 @@ class QuadSTests : InstrumentedTest { testHelper.runBlockingTest { try { - aliceSession.sharedSecretStorageService().getSecret("my.secret", + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", keyId1, RawBytesKeySpec.fromPassphrase( "A bad passphrase", key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, - null) + null + ) ) } catch (throwable: Throwable) { assert(throwable is SharedSecretStorageError.BadMac) @@ -260,13 +264,15 @@ class QuadSTests : InstrumentedTest { // Now try with correct key testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret("my.secret", + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", keyId1, RawBytesKeySpec.fromPassphrase( passphrase, key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, - null) + null + ) ) } @@ -321,7 +327,8 @@ class QuadSTests : InstrumentedTest { keyId, passphrase, emptyKeySigner, - null) + null + ) } assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index 14e659e2b6..2892cf8464 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -75,10 +75,12 @@ class SASTest : InstrumentedTest { } bobVerificationService.addListener(bobListener) - val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, + val txID = aliceVerificationService.beginKeyVerification( + VerificationMethod.SAS, bobSession.myUserId, bobSession.cryptoService().getMyDevice().deviceId, - null) + null + ) assertNotNull("Alice should have a started transaction", txID) val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) @@ -467,8 +469,10 @@ class SASTest : InstrumentedTest { val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction - assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), - bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) + assertEquals( + "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), + bobTx.getShortCodeRepresentation(SasMode.DECIMAL) + ) cryptoTestData.cleanUp(testHelper) } @@ -544,7 +548,8 @@ class SASTest : InstrumentedTest { // Assert that devices are verified val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = + bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) @@ -611,14 +616,16 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, bobSession.myUserId, - bobSession.sessionParams.deviceId!!) + bobSession.sessionParams.deviceId!! + ) bobVerificationService.beginKeyVerificationInDMs( VerificationMethod.SAS, requestID!!, cryptoTestData.roomId, aliceSession.myUserId, - aliceSession.sessionParams.deviceId!!) + aliceSession.sessionParams.deviceId!! + ) // we should reach SHOW SAS on both var alicePovTx: SasVerificationTransaction? diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt index 76bf6dc040..d7b4d636fc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -37,7 +37,8 @@ class QrCodeTest : InstrumentedTest { sharedSecret = "MTIzNDU2Nzg" ) - private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + private val value1 = + "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( transactionId = "MaTransaction", @@ -46,7 +47,8 @@ class QrCodeTest : InstrumentedTest { sharedSecret = "MTIzNDU2Nzg" ) - private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + private val value2 = + "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( transactionId = "MaTransaction", @@ -55,7 +57,8 @@ class QrCodeTest : InstrumentedTest { sharedSecret = "MTIzNDU2Nzg" ) - private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" + private val value3 = + "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 374d709505..df3b2ffe27 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -40,7 +39,6 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) -@Ignore("This test is flaky ; see issue #5449") class VerificationTest : InstrumentedTest { data class ExpectedResult( @@ -175,7 +173,8 @@ class VerificationTest : InstrumentedTest { ) ) } - }, callback) + }, callback + ) } testHelper.doSync { callback -> @@ -191,7 +190,8 @@ class VerificationTest : InstrumentedTest { ) ) } - }, callback) + }, callback + ) } val aliceVerificationService = aliceSession.cryptoService().verificationService() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index ef98ed22c7..acb23bf723 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -71,10 +71,12 @@ class MarkdownParserTest : InstrumentedTest { testIdentity("") testIdentity("a") testIdentity("1") - testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + - "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" + - "modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" + - "atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + testIdentity( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" + + "modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" + + "atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ) } @Test @@ -294,16 +296,20 @@ class MarkdownParserTest : InstrumentedTest { "$markdownPattern$name$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name" + ) } // Test twice the same tag "$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis" + ) } val textBefore = "a" @@ -313,48 +319,60 @@ class MarkdownParserTest : InstrumentedTest { "$textBefore$markdownPattern$name$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "$textBefore<$htmlExpectedTag>$name") + .expect( + expectedText = it, + expectedFormattedText = "$textBefore<$htmlExpectedTag>$name" + ) } // With text before and space "$textBefore $markdownPattern$name$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "$textBefore <$htmlExpectedTag>$name") + .expect( + expectedText = it, + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name" + ) } // With sticked text after "$markdownPattern$name$markdownPattern$textAfter" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name$textAfter") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name$textAfter" + ) } // With space and text after "$markdownPattern$name$markdownPattern $textAfter" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name $textAfter") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name $textAfter" + ) } // With sticked text before and text after "$textBefore$markdownPattern$name$markdownPattern$textAfter" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter") + .expect( + expectedText = it, + expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter" + ) } // With text before and after, with spaces "$textBefore $markdownPattern$name$markdownPattern $textAfter" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter") + .expect( + expectedText = it, + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter" + ) } } @@ -366,16 +384,20 @@ class MarkdownParserTest : InstrumentedTest { "$markdownPattern$name\n$name$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name$softBreak$name") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name$softBreak$name" + ) } // With new line between two blocks "$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern" .let { markdownParser.parse(it) - .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name
<$htmlExpectedTag>$name") + .expect( + expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name
<$htmlExpectedTag>$name" + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt index d38afc6b62..67eafea55d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt @@ -39,27 +39,35 @@ internal class JsonCanonicalizerTest : InstrumentedTest { """{"a":["c":"b","d":"e"]}""", """{"a":["d":"b","c":"e"]}""" ).forEach { - assertEquals(it, - JsonCanonicalizer.canonicalize(it)) + assertEquals( + it, + JsonCanonicalizer.canonicalize(it) + ) } } @Test fun reorderTest() { - assertEquals("""{"a":true,"b":false}""", - JsonCanonicalizer.canonicalize("""{"b":false,"a":true}""")) + assertEquals( + """{"a":true,"b":false}""", + JsonCanonicalizer.canonicalize("""{"b":false,"a":true}""") + ) } @Test fun realSampleTest() { - assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""", - JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}""")) + assertEquals( + """{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""", + JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}""") + ) } @Test fun doubleQuoteTest() { - assertEquals("{\"a\":\"\\\"\"}", - JsonCanonicalizer.canonicalize("{\"a\":\"\\\"\"}")) + assertEquals( + "{\"a\":\"\\\"\"}", + JsonCanonicalizer.canonicalize("{\"a\":\"\\\"\"}") + ) } /* ========================================================================================== @@ -68,38 +76,52 @@ internal class JsonCanonicalizerTest : InstrumentedTest { @Test fun matrixOrg001Test() { - assertEquals("""{}""", - JsonCanonicalizer.canonicalize("""{}""")) + assertEquals( + """{}""", + JsonCanonicalizer.canonicalize("""{}""") + ) } @Test fun matrixOrg002Test() { - assertEquals("""{"one":1,"two":"Two"}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"one":1,"two":"Two"}""", + JsonCanonicalizer.canonicalize( + """{ "one": 1, "two": "Two" -}""")) +}""" + ) + ) } @Test fun matrixOrg003Test() { - assertEquals("""{"a":"1","b":"2"}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize( + """{ "b": "2", "a": "1" -}""")) +}""" + ) + ) } @Test fun matrixOrg004Test() { - assertEquals("""{"a":"1","b":"2"}""", - JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}""")) + assertEquals( + """{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}""") + ) } @Test fun matrixOrg005Test() { - assertEquals("""{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""", + JsonCanonicalizer.canonicalize( + """{ "auth": { "success": true, "mxid": "@john.doe:example.com", @@ -117,37 +139,53 @@ internal class JsonCanonicalizerTest : InstrumentedTest { ] } } -}""")) +}""" + ) + ) } @Test fun matrixOrg006Test() { - assertEquals("""{"a":"日本語"}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"a":"日本語"}""", + JsonCanonicalizer.canonicalize( + """{ "a": "日本語" -}""")) +}""" + ) + ) } @Test fun matrixOrg007Test() { - assertEquals("""{"日":1,"本":2}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"日":1,"本":2}""", + JsonCanonicalizer.canonicalize( + """{ "本": 2, "日": 1 -}""")) +}""" + ) + ) } @Test fun matrixOrg008Test() { - assertEquals("""{"a":"日"}""", - JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}")) + assertEquals( + """{"a":"日"}""", + JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}") + ) } @Test fun matrixOrg009Test() { - assertEquals("""{"a":null}""", - JsonCanonicalizer.canonicalize("""{ + assertEquals( + """{"a":null}""", + JsonCanonicalizer.canonicalize( + """{ "a": null -}""")) +}""" + ) + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt index 728986441a..b5870ebf69 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt @@ -26,9 +26,18 @@ class StringOrderTest { @Test fun testbasing() { - assertEquals("a", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("a", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET)) - assertEquals("element", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("element", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET)) - assertEquals("matrix", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("matrix", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET)) + assertEquals( + "a", + StringOrderUtils.baseToString(StringOrderUtils.stringToBase("a", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET) + ) + assertEquals( + "element", + StringOrderUtils.baseToString(StringOrderUtils.stringToBase("element", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET) + ) + assertEquals( + "matrix", + StringOrderUtils.baseToString(StringOrderUtils.stringToBase("matrix", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET) + ) } @Test diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt index f6e08a576e..a2984dd27e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt @@ -59,7 +59,8 @@ class ThreadMessagingTest : InstrumentedTest { val sentMessages = commonTestHelper.sendTextMessage( room = aliceRoom, message = textMessage, - nbOfMessages = 1) + nbOfMessages = 1 + ) val initMessage = sentMessages.first() @@ -73,7 +74,8 @@ class ThreadMessagingTest : InstrumentedTest { room = aliceRoom, message = "Reply In the above thread", numberOfMessages = 1, - rootThreadEventId = initMessage.root.eventId.orEmpty()) + rootThreadEventId = initMessage.root.eventId.orEmpty() + ) val replyInThread = repliesInThread.first() replyInThread.root.isThread().shouldBeTrue() @@ -116,7 +118,8 @@ class ThreadMessagingTest : InstrumentedTest { val sentMessages = commonTestHelper.sendTextMessage( room = aliceRoom, message = textMessage, - nbOfMessages = 1) + nbOfMessages = 1 + ) val initMessage = sentMessages.first() @@ -134,7 +137,8 @@ class ThreadMessagingTest : InstrumentedTest { room = bobRoom, message = "Reply In the above thread", numberOfMessages = 1, - rootThreadEventId = initMessage.root.eventId.orEmpty()) + rootThreadEventId = initMessage.root.eventId.orEmpty() + ) val replyInThread = repliesInThread.first() replyInThread.root.isThread().shouldBeTrue() @@ -190,7 +194,8 @@ class ThreadMessagingTest : InstrumentedTest { val sentMessages = commonTestHelper.sendTextMessage( room = aliceRoom, message = textMessage, - nbOfMessages = 5) + nbOfMessages = 5 + ) sentMessages.forEach { it.root.isThread().shouldBeFalse() @@ -206,7 +211,8 @@ class ThreadMessagingTest : InstrumentedTest { room = aliceRoom, message = "Reply In the above thread", numberOfMessages = 40, - rootThreadEventId = selectedInitMessage.root.eventId.orEmpty()) + rootThreadEventId = selectedInitMessage.root.eventId.orEmpty() + ) repliesInThread.forEach { it.root.isThread().shouldBeTrue() @@ -253,7 +259,8 @@ class ThreadMessagingTest : InstrumentedTest { val sentMessages = commonTestHelper.sendTextMessage( room = aliceRoom, message = textMessage, - nbOfMessages = 5) + nbOfMessages = 5 + ) sentMessages.forEach { it.root.isThread().shouldBeFalse() @@ -270,7 +277,8 @@ class ThreadMessagingTest : InstrumentedTest { room = aliceRoom, message = "Alice reply In the above second thread message", numberOfMessages = 35, - rootThreadEventId = secondMessage.root.eventId.orEmpty()) + rootThreadEventId = secondMessage.root.eventId.orEmpty() + ) // Let's reply in timeline to that message from another user val bobSession = cryptoTestData.secondSession!! @@ -282,14 +290,16 @@ class ThreadMessagingTest : InstrumentedTest { room = bobRoom, message = "Bob reply In the above first thread message", numberOfMessages = 42, - rootThreadEventId = firstMessage.root.eventId.orEmpty()) + rootThreadEventId = firstMessage.root.eventId.orEmpty() + ) // Bob will also reply in second thread 5 times val bobThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage( room = bobRoom, message = "Another Bob reply In the above second thread message", numberOfMessages = 20, - rootThreadEventId = secondMessage.root.eventId.orEmpty()) + rootThreadEventId = secondMessage.root.eventId.orEmpty() + ) aliceThreadRepliesInSecondMessage.forEach { it.root.isThread().shouldBeTrue() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt index 5c011c8b2f..94b2ba55a3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.SessionRealmModule import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent @@ -42,6 +43,7 @@ import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMes internal class ChunkEntityTest : InstrumentedTest { private lateinit var monarchy: Monarchy + private val clock = DefaultClock() @Before fun setup() { @@ -59,14 +61,15 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } chunk.addTimelineEvent( roomId = ROOM_ID, eventEntity = fakeEvent, direction = PaginationDirection.FORWARDS, - roomMemberContentsByUser = emptyMap()) + roomMemberContentsByUser = emptyMap() + ) chunk.timelineEvents.size shouldBeEqualTo 1 } } @@ -75,19 +78,21 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } chunk.addTimelineEvent( roomId = ROOM_ID, eventEntity = fakeEvent, direction = PaginationDirection.FORWARDS, - roomMemberContentsByUser = emptyMap()) + roomMemberContentsByUser = emptyMap() + ) chunk.addTimelineEvent( roomId = ROOM_ID, eventEntity = fakeEvent, direction = PaginationDirection.FORWARDS, - roomMemberContentsByUser = emptyMap()) + roomMemberContentsByUser = emptyMap() + ) chunk.timelineEvents.size shouldBeEqualTo 1 } } @@ -153,14 +158,15 @@ internal class ChunkEntityTest : InstrumentedTest { events: List, direction: PaginationDirection) { events.forEach { event -> - val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let { + val fakeEvent = event.toEntity(roomId, SendState.SYNCED, clock.epochMillis()).let { realm.copyToRealm(it) } addTimelineEvent( roomId = roomId, eventEntity = fakeEvent, direction = direction, - roomMemberContentsByUser = emptyMap()) + roomMemberContentsByUser = emptyMap() + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt index b86c86c0c7..ccf1c7c2c9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt @@ -26,8 +26,8 @@ internal class FakeGetContextOfEventTask constructor(private val tokenChunkEvent override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent( - Random.nextLong(System.currentTimeMillis()).toString(), - Random.nextLong(System.currentTimeMillis()).toString(), + Random.nextLong().toString(), + Random.nextLong().toString(), fakeEvents ) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt index d09bfb18c6..f241be0c5c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt @@ -25,7 +25,7 @@ internal class FakePaginationTask @Inject constructor(private val tokenChunkEven override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) - val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) + val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong().toString(), fakeEvents) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index 3864ea1cd1..d5b4a07fc0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -71,7 +71,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { val sentMessages = commonTestHelper.sendTextMessage( roomFromAlicePOV, message, - numberOfMessagesToSend) + numberOfMessagesToSend + ) // Alice clear the cache and restart the sync commonTestHelper.clearCacheAndSync(aliceSession) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index 5d09b74e6c..6e5fed8df9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -94,7 +94,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { val firstMessageFromAliceId = commonTestHelper.sendTextMessage( roomFromAlicePOV, firstMessage, - 30) + 30 + ) .last() .eventId @@ -130,7 +131,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { commonTestHelper.sendTextMessage( roomFromAlicePOV, secondMessage, - 30) + 30 + ) // Bob start to sync bobSession.startSync(true) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index 251b2c614c..42f710d7cf 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -64,7 +64,8 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { commonTestHelper.sendTextMessage( roomFromAlicePOV, message, - numberOfMessagesToSent) + numberOfMessagesToSent + ) val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30)) bobTimeline.start() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index ab0bbe7f73..e17b7efbd6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -85,7 +85,8 @@ class SearchMessagesTest : InstrumentedTest { commonTestHelper.sendTextMessage( roomFromAlicePOV, MESSAGE, - 2) + 2 + ) val data = commonTestHelper.runBlockingTest { block.invoke(cryptoTestData) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index d2c8b52fc7..6a17cb74ad 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -177,21 +177,27 @@ class SpaceHierarchyTest : InstrumentedTest { val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) - val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + val spaceAInfo = createPublicSpace( + session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A2", true, true) - )) + ) + ) - /* val spaceBInfo = */ createPublicSpace(session, "SpaceB", listOf( + /* val spaceBInfo = */ createPublicSpace( + session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B2", true, true), Triple("B3", true, true) - )) + ) + ) - val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + val spaceCInfo = createPublicSpace( + session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C2", true, true) - )) + ) + ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) @@ -254,15 +260,19 @@ class SpaceHierarchyTest : InstrumentedTest { val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) - val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + val spaceAInfo = createPublicSpace( + session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A2", true, true) - )) + ) + ) - val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + val spaceCInfo = createPublicSpace( + session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C2", true, true) - )) + ) + ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) @@ -296,16 +306,20 @@ class SpaceHierarchyTest : InstrumentedTest { val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) - val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + val spaceAInfo = createPublicSpace( + session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A2", true, true) - )) + ) + ) - val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + val spaceBInfo = createPublicSpace( + session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B2", true, true), Triple("B3", true, true) - )) + ) + ) // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) @@ -315,10 +329,12 @@ class SpaceHierarchyTest : InstrumentedTest { session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) } - val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + val spaceCInfo = createPublicSpace( + session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C2", true, true) - )) + ) + ) commonTestHelper.waitWithLatch { latch -> @@ -446,21 +462,27 @@ class SpaceHierarchyTest : InstrumentedTest { val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) - /* val spaceAInfo = */ createPublicSpace(session, "SpaceA", listOf( + /* val spaceAInfo = */ createPublicSpace( + session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A2", true, true) - )) + ) + ) - val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + val spaceBInfo = createPublicSpace( + session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B2", true, true), Triple("B3", true, true) - )) + ) + ) - val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + val spaceCInfo = createPublicSpace( + session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C2", true, true) - )) + ) + ) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") @@ -494,10 +516,12 @@ class SpaceHierarchyTest : InstrumentedTest { val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) - val spaceAInfo = createPrivateSpace(aliceSession, "Private Space A", listOf( + val spaceAInfo = createPrivateSpace( + aliceSession, "Private Space A", listOf( Triple("General", true /*suggested*/, true/*canonical*/), Triple("Random", true, true) - )) + ) + ) commonTestHelper.runBlockingTest { aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt index 0efecbbe8a..83fcae7190 100644 --- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt @@ -28,8 +28,12 @@ internal class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContex val display = node.javaClass == DisplayMaths::class.java val contents = node.firstChild // should be the only child val latex = (contents as Text).literal - val attributes = context.extendAttributes(node, if (display) "div" else "span", Collections.singletonMap("data-mx-maths", - latex)) + val attributes = context.extendAttributes( + node, if (display) "div" else "span", Collections.singletonMap( + "data-mx-maths", + latex + ) + ) html.tag(if (display) "div" else "span", attributes) html.tag("code") context.render(contents) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 217f7e3da8..e7d1e64a2b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -132,9 +132,11 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() instance = Matrix(appContext, matrixConfiguration) } else { - throw IllegalStateException("Matrix is not initialized properly." + - " If you want to manage your own Matrix instance use Matrix.createInstance" + - " otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider.") + throw IllegalStateException( + "Matrix is not initialized properly." + + " If you want to manage your own Matrix instance use Matrix.createInstance" + + " otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider." + ) } } return instance diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/converter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/converter.kt index 1a4c1ee51c..80630bc4e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/converter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/converter.kt @@ -1,127 +1,129 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.auth - -import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms -import org.matrix.android.sdk.api.auth.registration.TermPolicies - -/** - * This method extract the policies from the login terms parameter, regarding the user language. - * For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable) - * - * Example of Data: - *
- * "m.login.terms": {
- *       "policies": {
- *         "privacy_policy": {
- *           "version": "1.0",
- *           "en": {
- *             "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
- *             "name": "Terms and Conditions"
- *           }
- *         }
- *       }
- *     }
- *
- * - * @param userLanguage the user language - * @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse - */ -fun TermPolicies.toLocalizedLoginTerms(userLanguage: String, - defaultLanguage: String = "en"): List { - val result = ArrayList() - - val policies = get("policies") - if (policies is Map<*, *>) { - policies.keys.forEach { policyName -> - val localizedFlowDataLoginTermsPolicyName = policyName as String - var localizedFlowDataLoginTermsVersion: String? = null - var localizedFlowDataLoginTermsLocalizedUrl: String? = null - var localizedFlowDataLoginTermsLocalizedName: String? = null - - val policy = policies[policyName] - - // Enter this policy - if (policy is Map<*, *>) { - // Version - localizedFlowDataLoginTermsVersion = policy["version"] as String? - - var userLanguageUrlAndName: UrlAndName? = null - var defaultLanguageUrlAndName: UrlAndName? = null - var firstUrlAndName: UrlAndName? = null - - // Search for language - policy.keys.forEach { policyKey -> - when (policyKey) { - "version" -> Unit // Ignore - userLanguage -> { - // We found the data for the user language - userLanguageUrlAndName = extractUrlAndName(policy[policyKey]) - } - defaultLanguage -> { - // We found default language - defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey]) - } - else -> { - if (firstUrlAndName == null) { - // Get at least some data - firstUrlAndName = extractUrlAndName(policy[policyKey]) - } - } - } - } - - // Copy found language data by priority - when { - userLanguageUrlAndName != null -> { - localizedFlowDataLoginTermsLocalizedUrl = userLanguageUrlAndName!!.url - localizedFlowDataLoginTermsLocalizedName = userLanguageUrlAndName!!.name - } - defaultLanguageUrlAndName != null -> { - localizedFlowDataLoginTermsLocalizedUrl = defaultLanguageUrlAndName!!.url - localizedFlowDataLoginTermsLocalizedName = defaultLanguageUrlAndName!!.name - } - firstUrlAndName != null -> { - localizedFlowDataLoginTermsLocalizedUrl = firstUrlAndName!!.url - localizedFlowDataLoginTermsLocalizedName = firstUrlAndName!!.name - } - } - } - - result.add(LocalizedFlowDataLoginTerms( - policyName = localizedFlowDataLoginTermsPolicyName, - version = localizedFlowDataLoginTermsVersion, - localizedUrl = localizedFlowDataLoginTermsLocalizedUrl, - localizedName = localizedFlowDataLoginTermsLocalizedName - )) - } - } - - return result -} - -private fun extractUrlAndName(policyData: Any?): UrlAndName? { - if (policyData is Map<*, *>) { - val url = policyData["url"] as String? - val name = policyData["name"] as String? - - if (url != null && name != null) { - return UrlAndName(url, name) - } - } - return null -} +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth + +import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms +import org.matrix.android.sdk.api.auth.registration.TermPolicies + +/** + * This method extract the policies from the login terms parameter, regarding the user language. + * For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable) + * + * Example of Data: + *
+ * "m.login.terms": {
+ *       "policies": {
+ *         "privacy_policy": {
+ *           "version": "1.0",
+ *           "en": {
+ *             "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
+ *             "name": "Terms and Conditions"
+ *           }
+ *         }
+ *       }
+ *     }
+ *
+ * + * @param userLanguage the user language + * @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse + */ +fun TermPolicies.toLocalizedLoginTerms(userLanguage: String, + defaultLanguage: String = "en"): List { + val result = ArrayList() + + val policies = get("policies") + if (policies is Map<*, *>) { + policies.keys.forEach { policyName -> + val localizedFlowDataLoginTermsPolicyName = policyName as String + var localizedFlowDataLoginTermsVersion: String? = null + var localizedFlowDataLoginTermsLocalizedUrl: String? = null + var localizedFlowDataLoginTermsLocalizedName: String? = null + + val policy = policies[policyName] + + // Enter this policy + if (policy is Map<*, *>) { + // Version + localizedFlowDataLoginTermsVersion = policy["version"] as String? + + var userLanguageUrlAndName: UrlAndName? = null + var defaultLanguageUrlAndName: UrlAndName? = null + var firstUrlAndName: UrlAndName? = null + + // Search for language + policy.keys.forEach { policyKey -> + when (policyKey) { + "version" -> Unit // Ignore + userLanguage -> { + // We found the data for the user language + userLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + defaultLanguage -> { + // We found default language + defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey]) + } + else -> { + if (firstUrlAndName == null) { + // Get at least some data + firstUrlAndName = extractUrlAndName(policy[policyKey]) + } + } + } + } + + // Copy found language data by priority + when { + userLanguageUrlAndName != null -> { + localizedFlowDataLoginTermsLocalizedUrl = userLanguageUrlAndName!!.url + localizedFlowDataLoginTermsLocalizedName = userLanguageUrlAndName!!.name + } + defaultLanguageUrlAndName != null -> { + localizedFlowDataLoginTermsLocalizedUrl = defaultLanguageUrlAndName!!.url + localizedFlowDataLoginTermsLocalizedName = defaultLanguageUrlAndName!!.name + } + firstUrlAndName != null -> { + localizedFlowDataLoginTermsLocalizedUrl = firstUrlAndName!!.url + localizedFlowDataLoginTermsLocalizedName = firstUrlAndName!!.name + } + } + } + + result.add( + LocalizedFlowDataLoginTerms( + policyName = localizedFlowDataLoginTermsPolicyName, + version = localizedFlowDataLoginTermsVersion, + localizedUrl = localizedFlowDataLoginTermsLocalizedUrl, + localizedName = localizedFlowDataLoginTermsLocalizedName + ) + ) + } + } + + return result +} + +private fun extractUrlAndName(policyData: Any?): UrlAndName? { + if (policyData is Map<*, *>) { + val url = policyData["url"] as String? + val name = policyData["name"] as String? + + if (url != null && name != null) { + return UrlAndName(url, name) + } + } + return null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt index 0da9eb4b7e..2b421f2873 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt @@ -88,8 +88,10 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult { val isMandatory = flows?.all { type in it.stages.orEmpty() } == true val stage = when (type) { - LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) - ?: "") + LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha( + isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) + ?: "" + ) LoginFlowTypes.DUMMY -> Stage.Dummy(isMandatory) LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap()) LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt index 9a686de2e1..9507ddda65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -31,5 +31,11 @@ data class MXCryptoConfig constructor( * If set to false, the request will be forwarded to the application layer; in this * case the application can decide to prompt the user. */ - val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true + val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true, + + /** + * Currently megolm keys are requested to the sender device and to all of our devices. + * You can limit request only to your sessions by turning this setting to `true` + */ + val limitRoomKeyRequestsToMyDevices: Boolean = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index d6d1248de7..b8c08d23dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse @@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -76,6 +75,15 @@ interface CryptoService { fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + /** + * Enable or disable key gossiping. + * Default is true. + * If set to false this device won't send key_request nor will accept key forwarded + */ + fun enableKeyGossiping(enable: Boolean) + + fun isKeyGossipingEnabled(): Boolean + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -94,8 +102,6 @@ interface CryptoService { fun reRequestRoomKeyForEvent(event: Event) - fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) - fun addRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener) @@ -142,14 +148,20 @@ interface CryptoService { fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getIncomingRoomKeyRequests(): List fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEvents(): List + /** + * Can be called by the app layer to accept a request manually + * Use carefully as it is prone to social attacks + */ + suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) + + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt new file mode 100644 index 0000000000..855f17a34f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto + +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode + +data class RequestReply( + val userId: String, + val fromDevice: String?, + val result: RequestResult +) + +sealed class RequestResult { + data class Success(val chainIndex: Int) : RequestResult() + data class Failure(val code: WithHeldCode) : RequestResult() +} + +data class OutgoingKeyRequest( + var requestBody: RoomKeyRequestBody?, + // recipients for the request map of users to list of deviceId + val recipients: Map>, + val fromIndex: Int, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + val requestId: String, // current state of this request + val state: OutgoingRoomKeyRequestState, + val results: List +) { + /** + * Used only for log. + * + * @return the room id. + */ + val roomId = requestBody?.roomId + + /** + * Used only for log. + * + * @return the session id + */ + val sessionId = requestBody?.sessionId +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt similarity index 55% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt index 2438e01102..6e80bdc133 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,20 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto +package org.matrix.android.sdk.api.session.crypto -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState +enum class OutgoingRoomKeyRequestState { + UNSENT, + SENT, + SENT_THEN_CANCELED, + CANCELLATION_PENDING, + CANCELLATION_PENDING_AND_WILL_RESEND; -internal interface OutgoingGossipingRequest { - val recipients: Map> - val requestId: String - val state: OutgoingGossipingRequestState - // transaction id for the cancellation, if any - // var cancellationTxnId: String? + companion object { + fun pendingStates() = setOf( + UNSENT, + CANCELLATION_PENDING_AND_WILL_RESEND, + CANCELLATION_PENDING + ) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt index 11996e673e..70ff76a4ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt @@ -93,7 +93,8 @@ data class CryptoCrossSigningKey( userId = userId, usages = listOf(usage.value), keys = mapOf("ed25519:$b64key" to b64key), - signatures = signMap) + signatures = signMap + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt index 3cd36c2ce8..24d3cf4004 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -16,9 +16,8 @@ package org.matrix.android.sdk.api.session.crypto.keyshare -import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest /** * Room keys events listener @@ -35,12 +34,12 @@ interface GossipingRequestListener { * Returns the secret value to be shared * @return true if is handled */ - fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean + fun onSecretShareRequest(request: SecretShareRequest): Boolean /** * A room key request cancellation has been received. * * @param request the cancellation request */ - fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) + fun onRequestCancelled(request: IncomingRoomKeyRequest) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt new file mode 100644 index 0000000000..861f3bd30b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode + +enum class TrailType { + OutgoingKeyForward, + IncomingKeyForward, + OutgoingKeyWithheld, + IncomingKeyRequest, + Unknown +} + +interface AuditInfo { + val roomId: String + val sessionId: String + val senderKey: String + val alg: String + val userId: String + val deviceId: String +} + +@JsonClass(generateAdapter = true) +data class ForwardInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + override val userId: String, + override val deviceId: String, + val chainIndex: Long? +) : AuditInfo + +object UnknownInfo : AuditInfo { + override val roomId: String = "" + override val sessionId: String = "" + override val senderKey: String = "" + override val alg: String = "" + override val userId: String = "" + override val deviceId: String = "" +} + +@JsonClass(generateAdapter = true) +data class WithheldInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + val code: WithHeldCode, + override val userId: String, + override val deviceId: String +) : AuditInfo + +@JsonClass(generateAdapter = true) +data class IncomingKeyRequestInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + override val userId: String, + override val deviceId: String, + val requestId: String +) : AuditInfo + +data class AuditTrail( + val ageLocalTs: Long, + val type: TrailType, + val info: AuditInfo +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt deleted file mode 100755 index 74ca7304f7..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.session.crypto.model - -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation - -/** - * IncomingRequestCancellation describes the incoming room key cancellation. - */ -data class IncomingRequestCancellation( - /** - * The user id - */ - override val userId: String? = null, - - /** - * The device id - */ - override val deviceId: String? = null, - - /** - * The request id - */ - override val requestId: String? = null, - override val localCreationTimestamp: Long? -) : IncomingShareRequestCommon { - companion object { - /** - * Factory - * - * @param event the event - */ - fun fromEvent(event: Event): IncomingRequestCancellation? { - return event.getClearContent() - .toModel() - ?.let { - IncomingRequestCancellation( - userId = event.senderId, - deviceId = it.requestingDeviceId, - requestId = it.requestId, - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt index 45b0926d89..0a28478a10 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt @@ -16,9 +16,7 @@ package org.matrix.android.sdk.api.session.crypto.model -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.util.time.Clock /** * IncomingRoomKeyRequest class defines the incoming room keys request. @@ -27,56 +25,62 @@ data class IncomingRoomKeyRequest( /** * The user id */ - override val userId: String? = null, + val userId: String? = null, /** * The device id */ - override val deviceId: String? = null, + val deviceId: String? = null, /** * The request id */ - override val requestId: String? = null, + val requestId: String? = null, /** * The request body */ val requestBody: RoomKeyRequestBody? = null, - val state: GossipingRequestState = GossipingRequestState.NONE, - - /** - * The runnable to call to accept to share the keys - */ - @Transient - var share: Runnable? = null, - - /** - * The runnable to call to ignore the key share request. - */ - @Transient - var ignore: Runnable? = null, - override val localCreationTimestamp: Long? -) : IncomingShareRequestCommon { + val localCreationTimestamp: Long? +) { companion object { /** * Factory * * @param event the event + * @param currentTimeMillis the current time in milliseconds */ - fun fromEvent(event: Event): IncomingRoomKeyRequest? { - return event.getClearContent() - .toModel() + fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? { + return trail + .takeIf { it.type == TrailType.IncomingKeyRequest } + ?.let { + it.info as? IncomingKeyRequestInfo + } ?.let { IncomingRoomKeyRequest( - userId = event.senderId, - deviceId = it.requestingDeviceId, + userId = it.userId, + deviceId = it.deviceId, requestId = it.requestId, - requestBody = it.body ?: RoomKeyRequestBody(), - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + requestBody = RoomKeyRequestBody( + algorithm = it.alg, + roomId = it.roomId, + senderKey = it.senderKey, + sessionId = it.sessionId + ), + localCreationTimestamp = trail.ageLocalTs ) } } + + internal fun fromRestRequest(senderId: String, request: RoomKeyShareRequest, clock: Clock): IncomingRoomKeyRequest? { + return IncomingRoomKeyRequest( + userId = senderId, + deviceId = request.requestingDeviceId, + requestId = request.requestId, + requestBody = request.body, + localCreationTimestamp = clock.epochMillis() + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt deleted file mode 100755 index 5afffef1ae..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.session.crypto.model - -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon - -/** - * IncomingSecretShareRequest class defines the incoming secret keys request. - */ -data class IncomingSecretShareRequest( - /** - * The user id - */ - override val userId: String? = null, - - /** - * The device id - */ - override val deviceId: String? = null, - - /** - * The request id - */ - override val requestId: String? = null, - - /** - * The request body - */ - val secretName: String? = null, - - /** - * The runnable to call to accept to share the keys - */ - @Transient - var share: ((String) -> Unit)? = null, - - /** - * The runnable to call to ignore the key share request. - */ - @Transient - var ignore: Runnable? = null, - - override val localCreationTimestamp: Long? - -) : IncomingShareRequestCommon { - companion object { - /** - * Factory - * - * @param event the event - */ - fun fromEvent(event: Event): IncomingSecretShareRequest? { - return event.getClearContent() - .toModel() - ?.let { - IncomingSecretShareRequest( - userId = event.senderId, - deviceId = it.requestingDeviceId, - requestId = it.requestId, - secretName = it.secretName, - localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt deleted file mode 100755 index 5f35cc908f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.session.crypto.model - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -data class OutgoingRoomKeyRequest( - // RequestBody - val requestBody: RoomKeyRequestBody?, - // list of recipients for the request - override val recipients: Map>, - // Unique id for this request. Used for both - // an id within the request for later pairing with a cancellation, and for - // the transaction id when sending the to_device messages to our local - override val requestId: String, // current state of this request - override val state: OutgoingGossipingRequestState - // transaction id for the cancellation, if any - // override var cancellationTxnId: String? = null -) : OutgoingGossipingRequest { - - /** - * Used only for log. - * - * @return the room id. - */ - val roomId: String? - get() = requestBody?.roomId - - /** - * Used only for log. - * - * @return the session id - */ - val sessionId: String? - get() = requestBody?.sessionId -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index b9d0c0ad2c..ec67e4b31d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -129,11 +129,10 @@ interface VerificationService { private const val TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000 private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 - fun isValidRequest(age: Long?): Boolean { + fun isValidRequest(age: Long?, currentTimeMillis: Long): Boolean { if (age == null) return false - val now = System.currentTimeMillis() - val tooInThePast = now - TEN_MINUTES_IN_MILLIS - val tooInTheFuture = now + FIVE_MINUTES_IN_MILLIS + val tooInThePast = currentTimeMillis - TEN_MINUTES_IN_MILLIS + val tooInTheFuture = currentTimeMillis + FIVE_MINUTES_IN_MILLIS return age in tooInThePast..tooInTheFuture } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt index a577daf9e4..1eac1d6b2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt @@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent( /** * A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code. */ - @Json(name = "reason") val reason: String? = null + @Json(name = "reason") val reason: String? = null, + + /** + * the device ID of the device sending the m.room_key.withheld message + * MSC3735 + */ + @Json(name = "from_device") val fromDevice: String? = null ) { val code: WithHeldCode? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index e3ccbad249..8e930f2a50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -68,7 +68,8 @@ interface FileService { mxcUrl = messageContent.getFileUrl(), fileName = messageContent.getFileName(), mimeType = messageContent.mimeType, - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) /** * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt index 721a2bc8af..3bb8fad810 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -131,7 +131,7 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult - fun requestSecret(name: String, myOtherDeviceId: String) + suspend fun requestSecret(name: String, myOtherDeviceId: String) data class KeyRef( val keyId: String?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 8784d85c10..f1cfe3fee5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -379,24 +379,33 @@ internal class DefaultAuthenticationService @Inject constructor( throw MatrixIdFailure.InvalidMatrixId } - return getWellknownTask.execute(GetWellknownTask.Params( - domain = matrixId.getDomain(), - homeServerConnectionConfig = homeServerConnectionConfig) + return getWellknownTask.execute( + GetWellknownTask.Params( + domain = matrixId.getDomain(), + homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults() + ) ) } + private fun HomeServerConnectionConfig?.orWellKnownDefaults() = this ?: HomeServerConnectionConfig.Builder() + // server uri is ignored when doing a wellknown lookup as we use the matrix id domain instead + .withHomeServerUri("https://dummy.org") + .build() + override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, matrixId: String, password: String, initialDeviceName: String, deviceId: String?): Session { - return directLoginTask.execute(DirectLoginTask.Params( - homeServerConnectionConfig = homeServerConnectionConfig, - userId = matrixId, - password = password, - deviceName = initialDeviceName, - deviceId = deviceId - )) + return directLoginTask.execute( + DirectLoginTask.Params( + homeServerConnectionConfig = homeServerConnectionConfig, + userId = matrixId, + password = password, + deviceName = initialDeviceName, + deviceId = deviceId + ) + ) } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt index a92384b4ed..08d683af7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt @@ -21,9 +21,11 @@ import io.realm.annotations.RealmModule /** * Realm module for authentication classes */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ SessionParamsEntity::class, PendingSessionEntity::class - ]) + ] +) internal class AuthRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt index 8e4043c11b..1296ea7cc4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt @@ -44,7 +44,8 @@ internal class PendingSessionMapper @Inject constructor(moshi: Moshi) { resetPasswordData = resetPasswordData, currentSession = entity.currentSession, isRegistrationStarted = entity.isRegistrationStarted, - currentThreePidData = threePidData) + currentThreePidData = threePidData + ) } fun map(sessionData: PendingSessionData?): PendingSessionEntity? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt index 147c0e8be0..86929b1afe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt @@ -54,6 +54,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { sessionParams.userId, credentialsJson, homeServerConnectionConfigJson, - sessionParams.isTokenValid) + sessionParams.isTokenValid + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 590b333e90..890cb68aad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -120,21 +120,25 @@ internal class DefaultRegistrationWizard( RegisterAddThreePidTask.Params( threePid, pendingSessionData.clientSecret, - pendingSessionData.sendAttempt)) + pendingSessionData.sendAttempt + ) + ) pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) .also { pendingSessionStore.savePendingSessionData(it) } val params = RegistrationParams( auth = if (threePid is RegisterThreePid.Email) { - AuthParams.createForEmailIdentity(safeSession, + AuthParams.createForEmailIdentity( + safeSession, ThreePidCredentials( clientSecret = pendingSessionData.clientSecret, sid = response.sid ) ) } else { - AuthParams.createForMsisdnIdentity(safeSession, + AuthParams.createForMsisdnIdentity( + safeSession, ThreePidCredentials( clientSecret = pendingSessionData.clientSecret, sid = response.sid diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index d07d5ecd64..203dbcc60e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -74,8 +74,8 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { * Indicate if the homeserver support MSC3440 for threads */ internal fun Versions.doesServerSupportThreads(): Boolean { - return getMaxVersion() >= HomeServerVersion.v1_3_0 || - unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false + // TODO: Check for v1.3 or whichever spec version formally specifies MSC3440. + return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt deleted file mode 100644 index 4380e31bff..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import javax.inject.Inject - -internal class CancelGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val requestId: String, - val recipients: Map>, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams { - companion object { - fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { - return Params( - sessionId = sessionId, - requestId = request.requestId, - recipients = request.recipients, - txnId = createUniqueTxnId(), - lastFailureMessage = null - ) - } - } - } - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - val contentMap = MXUsersDevicesMap() - val toDeviceContent = ShareRequestCancellation( - requestingDeviceId = credentials.deviceId, - requestId = params.requestId - ) - cryptoStore.saveGossipingEvent(Event( - type = EventType.ROOM_KEY_REQUEST, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = System.currentTimeMillis() - }) - - params.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - try { - cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING) - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = txnId - ) - ) - cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 6a57d94677..fd4bf6adfd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -42,12 +42,14 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse @@ -57,15 +59,13 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -76,7 +76,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService @@ -91,6 +90,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId @@ -103,6 +103,7 @@ import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -130,6 +131,7 @@ internal class DefaultCryptoService @Inject constructor( private val userId: String, @DeviceId private val deviceId: String?, + private val clock: Clock, private val myDeviceInfoHolder: Lazy, // the crypto store private val cryptoStore: IMXCryptoStore, @@ -154,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor( private val crossSigningService: DefaultCrossSigningService, // - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val incomingKeyRequestManager: IncomingKeyRequestManager, + private val secretShareManager: SecretShareManager, // - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, // Actions private val setDeviceVerificationAction: SetDeviceVerificationAction, private val megolmSessionDataImporter: MegolmSessionDataImporter, @@ -176,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor( private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, private val eventDecryptor: EventDecryptor, + private val verificationMessageProcessor: VerificationMessageProcessor, private val liveEventManager: Lazy ) : CryptoService { @@ -190,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor( } } - fun onLiveEvent(roomId: String, event: Event) { + fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) { // handle state events if (event.isStateEvent()) { when (event.type) { @@ -199,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor( EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } + + // handle verification + if (!isInitialSync) { + if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) { + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { + verificationMessageProcessor.process(event) + } + } + } } - val gossipingBuffer = mutableListOf() +// val gossipingBuffer = mutableListOf() override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask @@ -377,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor( // Open the store cryptoStore.open() - runCatching { -// if (isInitialSync) { -// // refresh the devices list for each known room members -// deviceListManager.invalidateAllDeviceLists() -// deviceListManager.refreshOutdatedDeviceLists() -// } else { - - // Why would we do that? it will be called at end of syn - incomingGossipingRequestManager.processReceivedGossipingRequests() -// } - }.fold( - { - isStarting.set(false) - isStarted.set(true) - }, - { - isStarting.set(false) - isStarted.set(false) - Timber.tag(loggerTag.value).e(it, "Start failed") - } - ) + isStarting.set(false) + isStarted.set(true) } /** @@ -405,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor( */ fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - incomingGossipingRequestManager.close() + incomingKeyRequestManager.close() + outgoingKeyRequestManager.close() olmDevice.release() cryptoStore.close() } @@ -470,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor( } oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingGossipingRequestManager.processReceivedGossipingRequests() } - } - tryOrNull { - gossipingBuffer.toList().let { - cryptoStore.saveGossipingEvents(it) + // Process pending key requests + try { + if (toDevices.isEmpty()) { + // this is not blocking + outgoingKeyRequestManager.requireProcessAllPendingKeyRequests() + } else { + Timber.tag(loggerTag.value) + .w("Don't process key requests yet as there might be more to_device to catchup") + } + } catch (failure: Throwable) { + // just for safety but should not throw + Timber.tag(loggerTag.value).w("failed to process pending request") + } + + try { + incomingKeyRequestManager.processIncomingRequests() + } catch (failure: Throwable) { + // just for safety but should not throw + Timber.tag(loggerTag.value).w("failed to process incoming room key requests") } - gossipingBuffer.clear() } } } @@ -592,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor( // (for now at least. Maybe we should alert the user somehow?) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) { + if (existingAlgorithm == algorithm) { // ignore Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") return false @@ -701,17 +709,19 @@ internal class DefaultCryptoService @Inject constructor( } val safeAlgorithm = alg if (safeAlgorithm != null) { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent() starts") runCatching { val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") MXEncryptEventContentResult(content, EventType.ENCRYPTED) }.foldToCallback(callback) } else { val algorithm = getEncryptionAlgorithm(roomId) - val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, - algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) + val reason = String.format( + MXCryptoError.UNABLE_TO_ENCRYPT_REASON, + algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON + ) Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason") callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) } @@ -783,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - gossipingBuffer.add(event) // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } - EventType.REQUEST_SECRET, + EventType.REQUEST_SECRET -> { + secretShareManager.handleSecretRequest(event) + } EventType.ROOM_KEY_REQUEST -> { - // save audit trail - gossipingBuffer.add(event) - // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) - incomingGossipingRequestManager.onGossipingRequestEvent(event) + event.getClearContent().toModel()?.let { req -> + // We'll always get these because we send room key requests to + // '*' (ie. 'all devices') which includes the sending device, + // so ignore requests from ourself because apart from it being + // very silly, it won't work because an Olm session cannot send + // messages to itself. + if (req.requestingDeviceId != deviceId) { // ignore self requests + event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) } + } + } } EventType.SEND_SECRET -> { - gossipingBuffer.add(event) onSecretSendReceived(event) } EventType.ROOM_KEY_WITHHELD -> { @@ -833,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor( val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") } - Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) - if (alg is IMXWithHeldExtension) { - alg.onRoomKeyWithHeldEvent(withHeldContent) - } else { - Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}") - return + val senderId = event.senderId ?: return Unit.also { + Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") } + withHeldContent.sessionId ?: return + withHeldContent.algorithm ?: return + withHeldContent.roomId ?: return + withHeldContent.senderKey ?: return + outgoingKeyRequestManager.onRoomKeyWithHeld( + sessionId = withHeldContent.sessionId, + algorithm = withHeldContent.algorithm, + roomId = withHeldContent.roomId, + senderKey = withHeldContent.senderKey, + fromDevice = withHeldContent.fromDevice, + event = Event( + type = EventType.ROOM_KEY_WITHHELD, + senderId = senderId, + content = event.getClearContent() + ) + ) } - private fun onSecretSendReceived(event: Event) { - Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") - if (!event.isEncrypted()) { - // secret send messages must be encrypted - Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event") - return - } - - // Was that sent by us? - if (event.senderId != userId) { - Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") - return - } - - val secretContent = event.getClearContent().toModel() ?: return - - val existingRequest = cryptoStore - .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } - - if (existingRequest == null) { - Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") - return - } - - if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { - // TODO Ask to application layer? - Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + private suspend fun onSecretSendReceived(event: Event) { + secretShareManager.onSecretSendReceived(event) { secretName, secretValue -> + handleSDKLevelGossip(secretName, secretValue) } } /** * Returns true if handled by SDK, otherwise should be sent to application layer */ - private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { + private fun handleSDKLevelGossip(secretName: String?, + secretValue: String): Boolean { return when (secretName) { MASTER_KEY_SSSS_NAME -> { crossSigningService.onSecretMSKGossip(secretValue) @@ -1022,9 +1026,9 @@ internal class DefaultCryptoService @Inject constructor( return withContext(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("importRoomKeys starts") - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) - val t1 = System.currentTimeMillis() + val t1 = clock.epochMillis() Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") @@ -1032,7 +1036,7 @@ internal class DefaultCryptoService @Inject constructor( .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) .fromJson(roomKeys) - val t2 = System.currentTimeMillis() + val t2 = clock.epochMillis() Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms") @@ -1091,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.setGlobalBlacklistUnverifiedDevices(block) } + override fun enableKeyGossiping(enable: Boolean) { + cryptoStore.enableKeyGossiping(enable) + } + + override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() + /** * Tells whether the client should ever send encrypted messages to unverified devices. * The default value is false. @@ -1154,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor( setRoomBlacklistUnverifiedDevices(roomId, false) } -// TODO Check if this method is still necessary - /** - * Cancel any earlier room key request - * - * @param requestBody requestBody - */ - override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody) - } - /** * Re request the encryption keys required to decrypt an event. * * @param event the event to decrypt again. */ override fun reRequestRoomKeyForEvent(event: Event) { - val wireContent = event.content.toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content") - } - - val requestBody = RoomKeyRequestBody( - algorithm = wireContent.algorithm, - roomId = event.roomId, - senderKey = wireContent.senderKey, - sessionId = wireContent.sessionId - ) - - outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + outgoingKeyRequestManager.requestKeyForEvent(event, true) } override fun requestRoomKeyForEvent(event: Event) { - val wireContent = event.content.toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") - } - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// if (!isStarted()) { -// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") -// internalStart(false) -// } - roomDecryptorProvider - .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) - ?.requestKeysForEvent(event, false) ?: run { - Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") - } - } + outgoingKeyRequestManager.requestKeyForEvent(event, false) } /** @@ -1208,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingGossipingRequestManager.addRoomKeysRequestListener(listener) + incomingKeyRequestManager.addRoomKeysRequestListener(listener) + secretShareManager.addListener(listener) } /** @@ -1217,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) + incomingKeyRequestManager.removeRoomKeysRequestListener(listener) + secretShareManager.removeListener(listener) } -// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { -// val deviceKey = deviceInfo.identityKey() -// -// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 -// val now = System.currentTimeMillis() -// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { -// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") -// return -// } -// -// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") -// lastNewSessionForcedDates.setObject(senderId, deviceKey, now) -// -// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) -// -// // Now send a blank message on that session so the other side knows about it. -// // (The keyshare request is sent in the clear so that won't do) -// // We send this first such that, as long as the toDevice messages arrive in the -// // same order we sent them, the other end will get this first, set up the new session, -// // then get the keyshare request and send the key over this new session (because it -// // is the session it has most recently received a message on). -// val payloadJson = mapOf("type" to EventType.DUMMY) -// -// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) -// val sendToDeviceMap = MXUsersDevicesMap() -// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) -// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") -// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) -// sendToDeviceTask.execute(sendToDeviceParams) -// } -// } - /** * Provides the list of unknown devices * @@ -1298,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor( return "DefaultCryptoService of $userId ($deviceId)" } - override fun getOutgoingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(): List { return cryptoStore.getOutgoingRoomKeyRequests() } - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { return cryptoStore.getOutgoingRoomKeyRequestsPaged() } - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getIncomingRoomKeyRequestsPaged() - } - override fun getIncomingRoomKeyRequests(): List { - return cryptoStore.getIncomingRoomKeyRequests() + return cryptoStore.getGossipingEvents() + .mapNotNull { + IncomingRoomKeyRequest.fromEvent(it) + } } - override fun getGossipingEventsTrail(): LiveData> { + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) { + IncomingRoomKeyRequest.fromEvent(it) + ?: IncomingRoomKeyRequest(localCreationTimestamp = 0L) + } + } + + /** + * If you registered a `GossipingRequestListener`, you will be notified of key request + * that was not accepted by the SDK. You can call back this manually to accept anyhow. + */ + override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { + incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request) + } + + override fun getGossipingEventsTrail(): LiveData> { return cryptoStore.getGossipingEventsTrail() } - override fun getGossipingEvents(): List { + override fun getGossipingEvents(): List { return cryptoStore.getGossipingEvents() } @@ -1342,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor( loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") - callback.onFailure(failure) - return@launch + // we probably shouldn't block sending on that (but questionable) + // but some members won't be able to decrypt } val userIds = getRoomUserIds(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 6cae2d0935..f546b35fcf 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -31,19 +31,23 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.logLimit +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject // Legacy name: MXDeviceList @SessionScope -internal class DeviceListManager @Inject constructor(private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val syncTokenStore: SyncTokenStore, - private val credentials: Credentials, - private val downloadKeysForUsersTask: DownloadKeysForUsersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor) { +internal class DeviceListManager @Inject constructor( + private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val syncTokenStore: SyncTokenStore, + private val credentials: Credentials, + private val downloadKeysForUsersTask: DownloadKeysForUsersTask, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor, + private val clock: Clock, +) { interface UserDevicesUpdateListener { fun onUsersDeviceUpdate(userIds: List) @@ -310,11 +314,20 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM stored } else { Timber.v("## CRYPTO | downloadKeys() : starts") - val t0 = System.currentTimeMillis() - val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") - result.also { - it.addEntriesFromMap(stored) + val t0 = clock.epochMillis() + try { + val result = doKeyDownloadForUsers(downloadUsers) + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") + result.also { + it.addEntriesFromMap(stored) + } + } catch (failure: Throwable) { + Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms") + if (forceDownload) { + throw failure + } else { + stored + } } } } @@ -475,8 +488,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (!isVerified) { - Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + - deviceKeys.deviceId + " with error " + errorMessage) + Timber.e( + "## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage + ) return false } @@ -486,9 +501,11 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // best off sticking with the original keys. // // Should we warn the user about it somehow? - Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + - deviceKeys.deviceId + " has changed : " + - previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + Timber.e( + "## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey + ) Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 1c8bce7377..a094189645 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -47,6 +48,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) internal class EventDecryptor @Inject constructor( private val cryptoCoroutineScope: CoroutineScope, private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, private val roomDecryptorProvider: RoomDecryptorProvider, private val messageEncrypter: MessageEncrypter, private val sendToDeviceTask: SendToDeviceTask, @@ -153,7 +155,7 @@ internal class EventDecryptor @Inject constructor( // we should force start a new session for those Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged") // get the one that should be retried according to rate limit - val now = System.currentTimeMillis() + val now = clock.epochMillis() val toUnwedge = wedgedDevices.filter { val lastForcedDate = lastNewSessionForcedDates[it] ?: 0 if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt deleted file mode 100644 index 0013c31eea..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.work.BackoffPolicy -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.ListenableWorker -import androidx.work.OneTimeWorkRequest -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.CancelableWork -import org.matrix.android.sdk.internal.worker.startChain -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@SessionScope -internal class GossipingWorkManager @Inject constructor( - private val workManagerProvider: WorkManagerProvider -) { - - inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .startChain(startChain) - .setInputData(data) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - } - - // Prevent sending queue to stay broken after app restart - // The unique queue id will stay the same as long as this object is instanciated - val queueSuffixApp = System.currentTimeMillis() - - fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { - workManagerProvider.workManager - .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest) - .enqueue() - - return CancelableWork(workManagerProvider.workManager, workRequest.id) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt deleted file mode 100644 index b907b57f82..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import timber.log.Timber -import java.util.concurrent.Executors -import javax.inject.Inject - -@SessionScope -internal class IncomingGossipingRequestManager @Inject constructor( - @SessionId private val sessionId: String, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val cryptoConfig: MXCryptoConfig, - private val gossipingWorkManager: GossipingWorkManager, - private val roomEncryptorsStore: RoomEncryptorsStore, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope) { - - private val executor = Executors.newSingleThreadExecutor() - - // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations - // we received in the current sync. - private val receivedGossipingRequests = ArrayList() - private val receivedRequestCancellations = ArrayList() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - init { - receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) - } - - fun close() { - executor.shutdownNow() - } - - // Recently verified devices (map of deviceId and timestamp) - private val recentlyVerifiedDevices = HashMap() - - /** - * Called when a session has been verified. - * This information can be used by the manager to decide whether or not to fullfil gossiping requests - */ - fun onVerificationCompleteForDevice(deviceId: String) { - // For now we just keep an in memory cache - synchronized(recentlyVerifiedDevices) { - recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() - } - } - - private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { - val verifTimestamp: Long? - synchronized(recentlyVerifiedDevices) { - verifTimestamp = recentlyVerifiedDevices[deviceId] - } - if (verifTimestamp == null) return false - - val age = System.currentTimeMillis() - verifTimestamp - - return age < FIVE_MINUTES_IN_MILLIS - } - - /** - * Called when we get an m.room_key_request event - * It must be called on CryptoThread - * - * @param event the announcement event. - */ - fun onGossipingRequestEvent(event: Event) { - val roomKeyShare = event.getClearContent().toModel() - Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare") - // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } - when (roomKeyShare?.action) { - GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { - if (event.getClearType() == EventType.REQUEST_SECRET) { - IncomingSecretShareRequest.fromEvent(event)?.let { - if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { - // ignore, it was sent by me as * - Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") - } else { -// // save in DB -// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) - receivedGossipingRequests.add(it) - } - } - } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { - IncomingRoomKeyRequest.fromEvent(event)?.let { - if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { - // ignore, it was sent by me as * - Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") - } else { -// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) - receivedGossipingRequests.add(it) - } - } - } - } - GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { - IncomingRequestCancellation.fromEvent(event)?.let { - receivedRequestCancellations.add(it) - } - } - else -> { - Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") - } - } - } - - /** - * Process any m.room_key_request or m.secret.request events which were queued up during the - * current sync. - * It must be called on CryptoThread - */ - fun processReceivedGossipingRequests() { - val roomKeyRequestsToProcess = receivedGossipingRequests.toList() - receivedGossipingRequests.clear() - - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process") - - var receivedRequestCancellations: List? = null - - synchronized(this.receivedRequestCancellations) { - if (this.receivedRequestCancellations.isNotEmpty()) { - receivedRequestCancellations = this.receivedRequestCancellations.toList() - this.receivedRequestCancellations.clear() - } - } - - executor.execute { - cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess) - for (request in roomKeyRequestsToProcess) { - if (request is IncomingRoomKeyRequest) { - processIncomingRoomKeyRequest(request) - } else if (request is IncomingSecretShareRequest) { - processIncomingSecretShareRequest(request) - } - } - - receivedRequestCancellations?.forEach { request -> - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") - // we should probably only notify the app of cancellations we told it - // about, but we don't currently have a record of that, so we just pass - // everything through. - if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { - // ignore remote echo - return@forEach - } - val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") - if (matchingIncoming == null) { - // ignore that? - return@forEach - } else { - // If it was accepted from this device, keep the information, do not mark as cancelled - if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { - onRoomKeyRequestCancellation(request) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) - } - } - } - } - } - - private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { - val userId = request.userId ?: return - val deviceId = request.deviceId ?: return - val body = request.requestBody ?: return - val roomId = body.roomId ?: return - val alg = body.algorithm ?: return - - Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") - if (credentials.userId != userId) { - handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId) - return - } - // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? - // if we don't have a decryptor for this room/alg, we don't have - // the keys for the requested events, and can drop the requests. - val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) - if (null == decryptor) { - Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - if (credentials.deviceId == deviceId && credentials.userId == userId) { - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - request.share = Runnable { - decryptor.shareKeysWithDevice(request) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) - } - request.ignore = Runnable { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - // if the device is verified already, share the keys - val device = cryptoStore.getUserDevice(userId, deviceId) - if (device != null) { - if (device.isVerified) { - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") - request.share?.run() - return - } - - if (device.isBlocked) { - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - } - - // As per config we automatically discard untrusted devices request - if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { - Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") - // At this point the device is unknown, we don't want to bother user with that - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - // Pass to application layer to decide what to do - onRoomKeyRequest(request) - } - - private fun handleKeyRequestFromOtherUser(body: RoomKeyRequestBody, - request: IncomingRoomKeyRequest, - alg: String, - roomId: String, - userId: String, - deviceId: String) { - Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user") - val senderKey = body.senderKey ?: return Unit - .also { Timber.w("missing senderKey") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - val sessionId = body.sessionId ?: return Unit - .also { Timber.w("missing sessionId") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - - if (alg != MXCRYPTO_ALGORITHM_MEGOLM) { - return Unit - .also { Timber.w("Only megolm is accepted here") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - } - - val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit - .also { Timber.w("no room Encryptor") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - if (roomEncryptor is IMXGroupEncryption) { - val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) - - if (isSuccess) { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) - } else { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) - } - } else { - Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg") - } - } - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED) - } - - private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { - val secretName = request.secretName ?: return Unit.also { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name") - } - - val userId = request.userId - if (userId == null || credentials.userId != userId) { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - val deviceId = request.deviceId - ?: return Unit.also { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - val device = cryptoStore.getUserDevice(userId, deviceId) - ?: return Unit.also { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - if (!device.isVerified || device.isBlocked) { - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() - - when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master - SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned - USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user - KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey - ?.let { - extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() - } - else -> null - }?.let { secretValue -> - Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") - if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { - val params = SendGossipWorker.Params( - sessionId = sessionId, - secretValue = secretValue, - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - txnId = createUniqueTxnId() - ) - - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - } else { - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - return - } - - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") - - request.ignore = Runnable { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - request.share = { secretValue -> - val params = SendGossipWorker.Params( - sessionId = userId, - secretValue = secretValue, - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - txnId = createUniqueTxnId() - ) - - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) - } - - onShareRequest(request) - } - - /** - * Dispatch onRoomKeyRequest - * - * @param request the request - */ - private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { - synchronized(gossipingRequestListeners) { - for (listener in gossipingRequestListeners) { - try { - listener.onRoomKeyRequest(request) - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed") - } - } - } - } - - /** - * Ask for a value to the listeners, and take the first one - */ - private fun onShareRequest(request: IncomingSecretShareRequest) { - synchronized(gossipingRequestListeners) { - for (listener in gossipingRequestListeners) { - try { - if (listener.onSecretShareRequest(request)) { - return - } - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed") - } - } - } - // Not handled, ignore - request.ignore?.run() - } - - /** - * A room key request cancellation has been received. - * - * @param request the cancellation request - */ - private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { - synchronized(gossipingRequestListeners) { - for (listener in gossipingRequestListeners) { - try { - listener.onRoomKeyRequestCancellation(request) - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed") - } - } - } - } - - fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - companion object { - private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt new file mode 100644 index 0000000000..13f2fb861a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt @@ -0,0 +1,463 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO) + +@SessionScope +internal class IncomingKeyRequestManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val olmDevice: MXOlmDevice, + private val cryptoConfig: MXCryptoConfig, + private val messageEncrypter: MessageEncrypter, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sendToDeviceTask: SendToDeviceTask, + private val clock: Clock, +) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) + val sequencer = SemaphoreCoroutineSequencer() + + private val incomingRequestBuffer = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + enum class MegolmRequestAction { + Request, Cancel + } + + data class ValidMegolmRequestBody( + val requestId: String, + val requestingUserId: String, + val requestingDeviceId: String, + val roomId: String, + val senderKey: String, + val sessionId: String, + val action: MegolmRequestAction + ) { + fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId" + } + + private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? { + val deviceId = requestingDeviceId ?: return null + val body = body ?: return null + val roomId = body.roomId ?: return null + val sessionId = body.sessionId ?: return null + val senderKey = body.senderKey ?: return null + val requestId = this.requestId ?: return null + if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + val action = when (this.action) { + "request" -> MegolmRequestAction.Request + "request_cancellation" -> MegolmRequestAction.Cancel + else -> null + } ?: return null + return ValidMegolmRequestBody( + requestId = requestId, + requestingUserId = senderId, + requestingDeviceId = deviceId, + roomId = roomId, + senderKey = senderKey, + sessionId = sessionId, + action = action + ) + } + + fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}") + return + } + outgoingRequestScope.launch { + // It is important to handle requests in order + sequencer.post { + val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also { + Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}") + } + + // is there already one like that? + val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest } + if (existing == null) { + when (validMegolmRequest.action) { + MegolmRequestAction.Request -> { + // just add to the buffer + incomingRequestBuffer.add(validMegolmRequest) + } + MegolmRequestAction.Cancel -> { + // ignore, we can't cancel as it's not known (probably already processed) + // still notify app layer if it was passed up previously + IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + listenersCopy.onEach { + tryOrNull { + withContext(coroutineDispatchers.main) { + it.onRequestCancelled(iReq) + } + } + } + } + } + } + } + } else { + when (validMegolmRequest.action) { + MegolmRequestAction.Request -> { + // it's already in buffer, nop keep existing + } + MegolmRequestAction.Cancel -> { + // discard the request in buffer + incomingRequestBuffer.remove(existing) + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + listenersCopy.onEach { + IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> + withContext(coroutineDispatchers.main) { + tryOrNull { it.onRequestCancelled(iReq) } + } + } + } + } + } + } + } + } + } + } + + fun processIncomingRequests() { + outgoingRequestScope.launch { + sequencer.post { + measureTimeMillis { + Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process") + incomingRequestBuffer.forEach { + // should not happen, we only store requests + if (it.action != MegolmRequestAction.Request) return@forEach + try { + handleIncomingRequest(it) + } catch (failure: Throwable) { + // ignore and continue, should not happen + Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it") + } + } + incomingRequestBuffer.clear() + }.let { duration -> + Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms") + } + } + } + } + + private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) { + // We don't want to download keys, if we don't know the device yet we won't share any how? + val requestingDevice = + cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}") + } + + cryptoStore.saveIncomingKeyRequestAuditTrail( + request.requestId, + request.roomId, + request.sessionId, + request.senderKey, + MXCRYPTO_ALGORITHM_MEGOLM, + request.requestingUserId, + request.requestingDeviceId + ) + + val roomAlgorithm = // withContext(coroutineDispatchers.crypto) { + cryptoStore.getRoomAlgorithm(request.roomId) +// } + if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + // strange we received a request for a room that is not encrypted + // maybe a broken state? + Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}") + return + } + + // Is it for one of our sessions? + if (request.requestingUserId == credentials.userId) { + Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}") + + if (request.requestingDeviceId == credentials.deviceId) { + // ignore it's a remote echo + return + } + // If it's verified we share from the early index we know + // if not we check if it was originaly shared or not + if (requestingDevice.isVerified) { + // we share from the earliest known chain index + shareMegolmKey(request, requestingDevice, null) + } else { + shareIfItWasPreviouslyShared(request, requestingDevice) + } + } else { + if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}") + return + } + Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}") + if (requestingDevice.isBlocked) { + // it's blocked, so send a withheld code + sendWithheldForRequest(request, WithHeldCode.BLACKLISTED) + } else { + shareIfItWasPreviouslyShared(request, requestingDevice) + } + } + } + + private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) { + // we don't reshare unless it was previously shared with + val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) { + cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice) + } + if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) { + // we share from the index it was previously shared with + shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong()) + } else { + val isOwnDevice = requestingDevice.userId == credentials.userId + sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED) + // if it's our device we could delegate to the app layer to decide + if (isOwnDevice) { + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + val iReq = IncomingRoomKeyRequest( + userId = requestingDevice.userId, + deviceId = requestingDevice.deviceId, + requestId = request.requestId, + requestBody = RoomKeyRequestBody( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + senderKey = request.senderKey, + sessionId = request.sessionId, + roomId = request.roomId + ), + localCreationTimestamp = clock.epochMillis() + ) + listenersCopy.onEach { + withContext(coroutineDispatchers.main) { + tryOrNull { it.onRoomKeyRequest(iReq) } + } + } + } + } + } + } + + private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) { + Timber.tag(loggerTag.value) + .w("Send withheld $code for req: ${request.shortDbgString()}") + val withHeldContent = RoomKeyWithHeldContent( + roomId = request.roomId, + senderKey = request.senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sessionId = request.sessionId, + codeString = code.value, + fromDevice = credentials.deviceId + ) + + val params = SendToDeviceTask.Params( + EventType.ROOM_KEY_WITHHELD, + MXUsersDevicesMap().apply { + setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent) + } + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(params) + Timber.tag(loggerTag.value) + .d("Send withheld $code req: ${request.shortDbgString()}") + } + + cryptoStore.saveWithheldAuditTrail( + roomId = request.roomId, + sessionId = request.sessionId, + senderKey = request.senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + code = code, + userId = request.requestingUserId, + deviceId = request.requestingDeviceId + ) + } catch (failure: Throwable) { + // Ignore it's not that important? + // do we want to fallback to a worker? + Timber.tag(loggerTag.value) + .w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}") + } + } + + suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { + request.requestId ?: return + request.deviceId ?: return + request.userId ?: return + request.requestBody?.roomId ?: return + request.requestBody.senderKey ?: return + request.requestBody.sessionId ?: return + val validReq = ValidMegolmRequestBody( + requestId = request.requestId, + requestingDeviceId = request.deviceId, + requestingUserId = request.userId, + roomId = request.requestBody.roomId, + senderKey = request.requestBody.senderKey, + sessionId = request.requestBody.sessionId, + action = MegolmRequestAction.Request + ) + val requestingDevice = + cryptoStore.getUserDevice(request.userId, request.deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}") + } + + shareMegolmKey(validReq, requestingDevice, null) + } + + private suspend fun shareMegolmKey(validRequest: ValidMegolmRequestBody, + requestingDevice: CryptoDeviceInfo, + chainIndex: Long?): Boolean { + Timber.tag(loggerTag.value) + .d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}") + + val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to establish olm session") + sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) + return false + } + + val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("reshareKey: no session with this device, probably because there were no one-time keys") + sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) + return false + } + val sessionHolder = try { + olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}") + // It's unavailable + sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE) + return false + } + + val export = sessionHolder.mutex.withLock { + sessionHolder.wrapper.exportKeys(chainIndex) + } ?: return false.also { + Timber.tag(loggerTag.value) + .e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}") + } + + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload) + Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + return try { + sendToDeviceTask.execute(sendToDeviceParams) + Timber.tag(loggerTag.value) + .i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + cryptoStore.saveForwardKeyAuditTrail( + validRequest.roomId, + validRequest.sessionId, + validRequest.senderKey, + MXCRYPTO_ALGORITHM_MEGOLM, + requestingDevice.userId, + requestingDevice.deviceId, + chainIndex + ) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + false + } + } + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + incomingRequestBuffer.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt index f8235bf344..89e38cb7cf 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -29,6 +29,7 @@ import javax.crypto.spec.SecretKeySpec import kotlin.experimental.and import kotlin.experimental.xor import kotlin.math.min +import kotlin.system.measureTimeMillis /** * Utility class to import/export the crypto data @@ -310,40 +311,40 @@ internal object MXMegolmExportEncryption { */ @Throws(Exception::class) private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { - val t0 = System.currentTimeMillis() - - // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm - // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. - // noticed as dklen/hlen - val prf = Mac.getInstance("HmacSHA512") - prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) - - // 512 bits key length val key = ByteArray(64) - val uc = ByteArray(64) + measureTimeMillis { + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + val prf = Mac.getInstance("HmacSHA512") + prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) - // U1 = PRF(Password, Salt || INT_32_BE(i)) - prf.update(salt) - val int32BE = ByteArray(4) { 0.toByte() } - int32BE[3] = 1.toByte() - prf.update(int32BE) - prf.doFinal(uc, 0) + // 512 bits key length + val uc = ByteArray(64) - // copy to the key - System.arraycopy(uc, 0, key, 0, uc.size) - - for (index in 2..iterations) { - // Uc = PRF(Password, Uc-1) - prf.update(uc) + // U1 = PRF(Password, Salt || INT_32_BE(i)) + prf.update(salt) + val int32BE = ByteArray(4) { 0.toByte() } + int32BE[3] = 1.toByte() + prf.update(int32BE) prf.doFinal(uc, 0) - // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in uc.indices) { - key[byteIndex] = key[byteIndex] xor uc[byteIndex] - } - } + // copy to the key + System.arraycopy(uc, 0, key, 0, uc.size) - Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms") + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in uc.indices) { + key[byteIndex] = key[byteIndex] xor uc[byteIndex] + } + } + }.also { + Timber.v("## deriveKeys() : $iterations in $it ms") + } return key } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 4947761f05..68a1519670 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.convertFromUTF8 import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmMessage @@ -42,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmSession import org.matrix.olm.OlmUtility import timber.log.Timber -import java.net.URLEncoder import javax.inject.Inject private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) @@ -55,7 +55,8 @@ internal class MXOlmDevice @Inject constructor( */ private val store: IMXCryptoStore, private val olmSessionStore: OlmSessionStore, - private val inboundGroupSessionStore: InboundGroupSessionStore + private val inboundGroupSessionStore: InboundGroupSessionStore, + private val clock: Clock, ) { val mutex = Mutex() @@ -277,7 +278,7 @@ internal class MXOlmDevice @Inject constructor( // Pretend we've received a message at this point, otherwise // if we try to send a message to the device, it won't use // this session - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) @@ -329,14 +330,6 @@ internal class MXOlmDevice @Inject constructor( Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") } - Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext") - try { - val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8")) - Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256") - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") - } - val olmMessage = OlmMessage() olmMessage.mCipherText = ciphertext olmMessage.mType = messageType.toLong() @@ -348,7 +341,7 @@ internal class MXOlmDevice @Inject constructor( val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) // This counts as a received message: set last received message time to now - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) } catch (e: Exception) { @@ -454,7 +447,7 @@ internal class MXOlmDevice @Inject constructor( payloadString = olmSessionWrapper.mutex.withLock { olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived() + olmSessionWrapper.onMessageReceived(clock.epochMillis()) } } olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) @@ -520,6 +513,7 @@ internal class MXOlmDevice @Inject constructor( return MXOutboundSessionInfo( sessionId = sessionId, sharedWithHelper = SharedWithHelper(roomId, sessionId, store), + clock, restoredOutboundGroupSession.creationTime ) } @@ -586,6 +580,13 @@ internal class MXOlmDevice @Inject constructor( // Inbound group session + sealed interface AddSessionResult { + data class Imported(val ratchetIndex: Int) : AddSessionResult + abstract class Failure : AddSessionResult + object NotImported : Failure() + data class NotImportedHigherIndex(val newIndex: Int) : Failure() + } + /** * Add an inbound group session to the session store. * @@ -604,7 +605,7 @@ internal class MXOlmDevice @Inject constructor( senderKey: String, forwardingCurve25519KeyChain: List, keysClaimed: Map, - exportFormat: Boolean): Boolean { + exportFormat: Boolean): AddSessionResult { val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper @@ -612,7 +613,7 @@ internal class MXOlmDevice @Inject constructor( if (existingSession != null) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") try { - val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also { + val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { // This is quite unexpected, could throw if native was released? Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") candidateSession.olmInboundGroupSession?.releaseSession() @@ -623,12 +624,12 @@ internal class MXOlmDevice @Inject constructor( if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") candidateSession.olmInboundGroupSession?.releaseSession() - return false + return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") candidateSession.olmInboundGroupSession?.releaseSession() - return false + return AddSessionResult.NotImported } } @@ -638,19 +639,19 @@ internal class MXOlmDevice @Inject constructor( val candidateOlmInboundSession = candidateSession.olmInboundGroupSession if (null == candidateOlmInboundSession) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") - return false + return AddSessionResult.NotImported } try { if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") candidateOlmInboundSession.releaseSession() - return false + return AddSessionResult.NotImported } } catch (e: Throwable) { candidateOlmInboundSession.releaseSession() Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") - return false + return AddSessionResult.NotImported } candidateSession.senderKey = senderKey @@ -664,7 +665,7 @@ internal class MXOlmDevice @Inject constructor( inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) } - return true + return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) } /** @@ -787,7 +788,7 @@ internal class MXOlmDevice @Inject constructor( if (timelineSet.contains(messageIndexKey)) { val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") + Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt index 792c9a25dc..8143e36892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import timber.log.Timber import javax.inject.Inject @@ -38,6 +39,7 @@ internal class OneTimeKeysUploader @Inject constructor( private val olmDevice: MXOlmDevice, private val objectSigner: ObjectSigner, private val uploadKeysTask: UploadKeysTask, + private val clock: Clock, context: Context ) { // tell if there is a OTK check in progress @@ -77,7 +79,7 @@ internal class OneTimeKeysUploader @Inject constructor( Timber.v("maybeUploadOneTimeKeys: already in progress") return } - if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { + if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { // we've done a key upload recently. Timber.v("maybeUploadOneTimeKeys: executed too recently") return @@ -94,7 +96,7 @@ internal class OneTimeKeysUploader @Inject constructor( Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}") - lastOneTimeKeyCheck = System.currentTimeMillis() + lastOneTimeKeyCheck = clock.epochMillis() // We then check how many keys we can store in the Account object. val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() @@ -126,7 +128,7 @@ internal class OneTimeKeysUploader @Inject constructor( // Check if we need to forget a fallback key val latestPublishedTime = getLastFallbackKeyPublishTime() - if (latestPublishedTime != 0L && System.currentTimeMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { + if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { // This should be called once you are reasonably certain that you will not receive any more messages // that use the old fallback key Timber.d("## forgetFallbackKey()") @@ -168,7 +170,7 @@ internal class OneTimeKeysUploader @Inject constructor( olmDevice.markKeysAsPublished() if (hadUnpublishedFallbackKey) { // It had an unpublished fallback key that was published just now - saveLastFallbackKeyPublishTime(System.currentTimeMillis()) + saveLastFallbackKeyPublishTime(clock.epochMillis()) } if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt deleted file mode 100755 index e6f6ac5053..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import timber.log.Timber -import javax.inject.Inject - -@SessionScope -internal class OutgoingGossipingRequestManager @Inject constructor( - @SessionId private val sessionId: String, - private val cryptoStore: IMXCryptoStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val gossipingWorkManager: GossipingWorkManager) { - - /** - * Send off a room key request, if we haven't already done so. - * - * - * The `requestBody` is compared (with a deep-equality check) against - * previous queued or sent requests and if it matches, no change is made. - * Otherwise, a request is added to the pending list, and a job is started - * in the background to send it. - * - * @param requestBody requestBody - * @param recipients recipients - */ - fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { - // Don't resend if it's already done, you need to cancel first (reRequest) - if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { - Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it") - return@launch - } - - sendOutgoingGossipingRequest(it) - } - } - } - - fun sendSecretShareRequest(secretName: String, recipients: Map>) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - // A bit dirty, but for better stability give other party some time to mark - // devices trusted :/ - delay(1500) - cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { - // TODO check if there is already one that is being sent? - if (it.state == OutgoingGossipingRequestState.SENDING - /**|| it.state == OutgoingGossipingRequestState.SENT*/ - ) { - Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it") - return@launch - } - - sendOutgoingGossipingRequest(it) - } - } - } - - /** - * Cancel room key requests, if any match the given details - * - * @param requestBody requestBody - */ - fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - cancelRoomKeyRequest(requestBody, false) - } - } - - /** - * Cancel room key requests, if any match the given details, and resend - * - * @param requestBody requestBody - */ - fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - cancelRoomKeyRequest(requestBody, true) - } - } - - /** - * Cancel room key requests, if any match the given details, and resend - * - * @param requestBody requestBody - * @param andResend true to resend the key request - */ - private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { - val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) // no request was made for this key - ?: return Unit.also { - Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody") - } - - sendOutgoingRoomKeyRequestCancellation(req, andResend) - } - - /** - * Send the outgoing key request. - * - * @param request the request - */ - private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { - Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request") - - val params = SendGossipRequestWorker.Params( - sessionId = sessionId, - keyShareRequest = request as? OutgoingRoomKeyRequest, - secretShareRequest = request as? OutgoingSecretRequest, - txnId = createUniqueTxnId() - ) - cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - } - - /** - * Given a OutgoingRoomKeyRequest, cancel it and delete the request record - * - * @param request the request - */ - private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) { - Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request") - val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) - cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) - - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - - if (resend) { - val reSendParams = SendGossipRequestWorker.Params( - sessionId = sessionId, - keyShareRequest = request.copy(requestId = RequestIdHelper.createUniqueRequestId()), - txnId = createUniqueTxnId() - ) - val reSendWorkRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(reSendParams), true) - gossipingWorkManager.postWork(reSendWorkRequest) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt new file mode 100755 index 0000000000..09a9868428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt @@ -0,0 +1,518 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import timber.log.Timber +import java.util.Stack +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) + +/** + * This class is responsible for sending key requests to other devices when a message failed to decrypt. + * It's lifecycle is based on the sync pulse: + * - You can post queries for session, or report when you got a session + * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices + * If a request failed it will be retried at the end of the next sync + */ +@SessionScope +internal class OutgoingKeyRequestManager @Inject constructor( + @SessionId private val sessionId: String, + @UserId private val myUserId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoConfig: MXCryptoConfig, + private val inboundGroupSessionStore: InboundGroupSessionStore, + private val sendToDeviceTask: SendToDeviceTask, + private val deviceListManager: DeviceListManager, + private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + // We only have one active key request per session, so we don't request if it's already requested + // But it could make sense to check more the backup, as it's evolving. + // We keep a stack as we consider that the key requested last is more likely to be on screen? + private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() + + fun requestKeyForEvent(event: Event, force: Boolean) { + val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return + val index = ratchetIndexForMessage(event) ?: 0 + postRoomKeyRequest(body, targets, index, force) + } + + private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { + val sender = event.senderId ?: return null + val encryptedEventContent = event.content.toModel() ?: return null.also { + Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") + } + if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + + val senderDevice = encryptedEventContent.deviceId + val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + mapOf( + myUserId to listOf("*") + ) + } else { + if (event.senderId == myUserId) { + mapOf( + myUserId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + myUserId to listOf("*"), + + // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 + // so in this case query to all + sender to listOf(senderDevice ?: "*") + ) + } + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + return recipients to requestBody + } + + private fun ratchetIndexForMessage(event: Event): Int? { + val encryptedContent = event.content.toModel() ?: return null + if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { + tryOrNull { + val megolmVersion = it.read() + if (megolmVersion != 3) return@tryOrNull null + /** Int tag */ + if (it.read() != 8) return@tryOrNull null + it.read() + } + } + } + + fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueRequest(requestBody, recipients, fromIndex, force) + } + } + } + + /** + * Typically called when we the session as been imported or received meanwhile + */ + fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) + } + } + } + + fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { + if (newTrust) { + // we were previously not cross signed, but we are now + // so there is now more chances to get better replies for existing request + // Let's forget about sent request so that next time we try to decrypt we will resend requests + // We don't resend all because we don't want to generate a bulk of traffic + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) + } + + sequencer.post { + delay(1000) + perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) + } + } + } + } + + fun onRoomKeyForwarded(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + fromIndex: Int, + event: Event) { + Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + // strip out encrypted stuff as it's just a trail? + event = event.copy( + type = event.getClearType(), + content = mapOf( + "chain_index" to fromIndex + ) + ) + ) + } + } + } + + fun onRoomKeyWithHeld(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + event: Event) { + outgoingRequestScope.launch { + sequencer.post { + Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") + Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") + + // We want to store withheld code from the sender of the message (owner of the megolm session), not from + // other devices that might gossip the key. If not the initial reason might be overridden + // by a request to one of our session. + event.getClearContent().toModel()?.let { withheld -> + withContext(coroutineDispatchers.crypto) { + tryOrNull { + deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) + } + cryptoStore.getUserDeviceList(event.senderId ?: "") + .also { devices -> + Timber.tag(loggerTag.value) + .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") + } + ?.firstOrNull { + it.identityKey() == senderKey + } + }.also { + Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") + }?.let { + if (it.userId == event.senderId) { + if (fromDevice != null) { + if (it.deviceId == fromDevice) { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } else { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } + } + } + + // Here we store the replies from a given request + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + event = event + ) + } + } + } + + /** + * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those) + */ + fun requireProcessAllPendingKeyRequests() { + outgoingRequestScope.launch { + sequencer.post { + internalProcessPendingKeyRequests() + } + } + } + + private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { + // do we have known requests for that session?? + Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") + val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey + ) + if (knownRequest.isEmpty()) return Unit.also { + Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") + } + if (knownRequest.size > 1) { + // It's worth logging, there should be only one + Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") + } + knownRequest.forEach { request -> + when (request.state) { + OutgoingRoomKeyRequestState.UNSENT -> { + if (request.fromIndex >= localKnownChainIndex) { + // we have a good index we can cancel + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + } + } + OutgoingRoomKeyRequestState.SENT -> { + // It was already sent, and index satisfied we can cancel + if (request.fromIndex >= localKnownChainIndex) { + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // It is already marked to be cancelled + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + if (request.fromIndex >= localKnownChainIndex) { + // we just want to cancel now + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + // was already canceled + // if we need a better index, should we resend? + } + } + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } + + private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { + if (!cryptoStore.isKeyGossipingEnabled()) { + // we might want to try backup? + if (requestBody.roomId != null && requestBody.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) + } + Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") + return + } + + Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") + val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") + when (existing?.state) { + null -> { + // create a new one + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + OutgoingRoomKeyRequestState.UNSENT -> { + // nothing it's new or not yet handled + } + OutgoingRoomKeyRequestState.SENT -> { + // it was already requested + Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") + if (force) { + // update to UNSENT + Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } else { + if (existing.roomId != null && existing.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) + } + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // request is canceled only if I got the keys so what to do here... + if (force) { + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + // It's already going to resend + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + if (force) { + cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + } + } + + if (existing != null && existing.fromIndex >= fromIndex) { + // update the required index + cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) + } + } + + private suspend fun internalProcessPendingKeyRequests() { + val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) + Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") + + measureTimeMillis { + toProcess.forEach { + when (it.state) { + OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, + OutgoingRoomKeyRequestState.SENT -> { + // these are filtered out + } + } + } + }.let { + Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") + } + + val maxBackupCallsBySync = 60 + var currentCalls = 0 + measureTimeMillis { + while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> + // we want to rate limit that somehow :/ + perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) + } + currentCalls++ + } + }.let { + Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") + } + } + + private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { + // In order to avoid generating to_device traffic, we can first check if the key is backed up + Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") + val sessionId = request.sessionId ?: return + val roomId = request.roomId ?: return + if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { + // let's see what's the index + val knownIndex = tryOrNull { + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + } + if (knownIndex != null && knownIndex <= request.fromIndex) { + // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request + Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + return + } + } + + // we need to send the request + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = request.requestBody + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") + // The request was sent, so update state + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") + } + } + + private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { + Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") + // we have to cancel this + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + return try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + // The request cancellation was sent, we don't delete yet because we want + // to keep trace of the sent replies + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") + false + } + } + + private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { + if (handleRequestToCancel(request)) { + // this will create a new unsent request with no replies that will be process in the following call + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt deleted file mode 100755 index 2ba2f5c817..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -internal class OutgoingSecretRequest( - // Secret Name - val secretName: String?, - // list of recipients for the request - override var recipients: Map>, - // Unique id for this request. Used for both - // an id within the request for later pairing with a cancellation, and for - // the transaction id when sending the to_device messages to our local - override var requestId: String, - // current state of this request - override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { - - // transaction id for the cancellation, if any -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt new file mode 100644 index 0000000000..0c059e7ca9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import dagger.Lazy +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.util.awaitCallback +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +// I keep the same name as OutgoingGossipingRequestManager to ease filtering of logs +private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.CRYPTO) + +/** + * Used to try to get the key for UISI messages before sending room key request. + * We are adding some rate limiting to avoid querying too much for a key not in backup. + * Nonetheless the backup can be updated so we might want to retry from time to time. + */ +internal class PerSessionBackupQueryRateLimiter @Inject constructor( + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val keysBackupService: Lazy, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, +) { + + companion object { + val MIN_TRY_BACKUP_PERIOD_MILLIS = 60 * 60_000 // 1 hour + } + + data class Info( + val megolmSessionId: String, + val roomId: String + ) + + data class LastTry( + val backupVersion: String, + val timestamp: Long + ) + + /** + * Remember what we already tried (a key not in backup or some server issue) + * We might want to retry from time to time as the backup could have been updated + */ + private val lastFailureMap = mutableMapOf() + + private var backupVersion: KeysVersionResult? = null + private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null + var backupWasCheckedFromServer: Boolean = false + val now = clock.epochMillis() + + fun refreshBackupInfoIfNeeded(force: Boolean = false) { + if (backupWasCheckedFromServer && !force) return + Timber.tag(loggerTag.value).v("Checking if can access a backup") + backupWasCheckedFromServer = true + val knownBackupSecret = cryptoStore.getKeyBackupRecoveryKeyInfo() + ?: return Unit.also { + Timber.tag(loggerTag.value).v("We don't have the backup secret!") + } + this.backupVersion = keysBackupService.get().keysBackupVersion + this.savedKeyBackupKeyInfo = knownBackupSecret + } + + suspend fun tryFromBackupIfPossible(sessionId: String, roomId: String): Boolean { + Timber.tag(loggerTag.value).v("tryFromBackupIfPossible for session:$sessionId in $roomId") + refreshBackupInfoIfNeeded() + val currentVersion = backupVersion + if (savedKeyBackupKeyInfo?.version == null || + currentVersion == null || + currentVersion.version != savedKeyBackupKeyInfo?.version) { + // We can't access the backup + Timber.tag(loggerTag.value).v("Can't get backup version info") + return false + } + val cacheKey = Info(sessionId, roomId) + val lastTry = lastFailureMap[cacheKey] + val shouldQuery = + lastTry == null || + lastTry.backupVersion != currentVersion.version || + (now - lastTry.timestamp) > MIN_TRY_BACKUP_PERIOD_MILLIS + + if (!shouldQuery) return false + + val successfullyImported = withContext(coroutineDispatchers.io) { + try { + awaitCallback { + keysBackupService.get().restoreKeysWithRecoveryKey( + currentVersion, + savedKeyBackupKeyInfo?.recoveryKey ?: "", + roomId, + sessionId, + null, + it + ) + }.successfullyNumberOfImportedKeys + } catch (failure: Throwable) { + // Fail silently + Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}") + 0 + } + } + if (successfullyImported == 1) { + Timber.tag(loggerTag.value).v("Found key in backup session:$sessionId in $roomId") + lastFailureMap.remove(cacheKey) + return true + } else { + Timber.tag(loggerTag.value).v("Failed to find key in backup session:$sessionId in $roomId") + lastFailureMap[cacheKey] = LastTry(currentVersion.version, clock.epochMillis()) + return false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt index ba97d96133..1a8c160d9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -16,12 +16,21 @@ package org.matrix.android.sdk.internal.crypto +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject @SessionScope -internal class RoomEncryptorsStore @Inject constructor() { +internal class RoomEncryptorsStore @Inject constructor( + private val cryptoStore: IMXCryptoStore, + private val megolmEncryptionFactory: MXMegolmEncryptionFactory, + private val olmEncryptionFactory: MXOlmEncryptionFactory, +) { // MXEncrypting instance for each room. private val roomEncryptors = mutableMapOf() @@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() { fun get(roomId: String): IMXEncrypting? { return synchronized(roomEncryptors) { - roomEncryptors[roomId] + val cache = roomEncryptors[roomId] + if (cache != null) { + return@synchronized cache + } else { + val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) + MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) + else -> null + } + alg?.let { roomEncryptors.put(roomId, it) } + return@synchronized alg + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt new file mode 100644 index 0000000000..6fb6914206 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) + +@SessionScope +internal class SecretShareManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, +) { + + companion object { + private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes + } + + /** + * Secret gossiping only occurs during a limited window period after interactive verification. + * We keep track of recent verification in memory for that purpose (no need to persist) + */ + private val recentlyVerifiedDevices = mutableMapOf() + private val verifMutex = Mutex() + + /** + * Secrets are exchanged as part of interactive verification, + * so we can just store in memory. + */ + private val outgoingSecretRequests = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + fun addListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfill gossiping requests. + * This should be called as fast as possible after a successful self interactive verification + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + cryptoCoroutineScope.launch { + verifMutex.withLock { + recentlyVerifiedDevices[deviceId] = clock.epochMillis() + } + } + } + + suspend fun handleSecretRequest(toDevice: Event) { + val request = toDevice.getClearContent().toModel() + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request") + } + +// val (action, requestingDeviceId, requestId, secretName) = it + val secretName = request.secretName ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing secret name") + } + + val userId = toDevice.senderId ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing senderId") + } + + if (userId != credentials.userId) { + // secrets are only shared between our own devices + Timber.tag(loggerTag.value) + .e("Ignoring secret share request from other users $userId") + return + } + + val deviceId = request.requestingDeviceId + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request norequestingDeviceId ") + } + + val device = cryptoStore.getUserDevice(credentials.userId, deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value) + .e("Received secret share request from unknown device $deviceId") + } + + val isRequestingDeviceTrusted = device.isVerified + val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) + if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { + // we can share the secret + + val secretValue = when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + } + if (secretValue == null) { + Timber.tag(loggerTag.value) + .i("The secret is unknown $secretName, passing to app layer") + val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } + toList.onEach { listener -> + listener.onSecretShareRequest(request) + } + return + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to mapOf( + "request_id" to request.requestId, + "secret" to secretValue + ) + ) + + // Is it possible that we don't have an olm session? + val devicesByUser = mapOf(device.userId to listOf(device)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Can't share secret ${request.secretName}: Failed to establish olm session") + return + } + + val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") + return + } + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + try { + // raise the retries for secret + sendToDeviceTask.executeRetry(sendToDeviceParams, 6) + Timber.tag(loggerTag.value) + .i("successfully shared secret $secretName to ${device.shortDebugString()}") + // TODO add a trail for that in audit logs + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") + } + } else { + Timber.tag(loggerTag.value) + .d(" Received secret share request from un-authorised device ${device.deviceId}") + } + } + + private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp = verifMutex.withLock { + recentlyVerifiedDevices[deviceId] + } ?: return false + + val age = clock.epochMillis() - verifTimestamp + + return age < SECRET_SHARE_WINDOW_DURATION + } + + suspend fun requestSecretTo(deviceId: String, secretName: String) { + val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { + Timber.tag(loggerTag.value) + .d("Can't request secret for $secretName unknown device $deviceId") + } + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + secretName = secretName, + requestId = createUniqueTxnId() + ) + + verifMutex.withLock { + outgoingSecretRequests.add(toDeviceContent) + } + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.REQUEST_SECRET, + contentMap = contentMap + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value) + .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") + } + } + + suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { + Timber.tag(loggerTag.value) + .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") + if (!toDevice.isEncrypted()) { + // secret send messages must be encrypted + Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (toDevice.senderId != credentials.userId) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") + return + } + + val secretContent = toDevice.getClearContent().toModel() ?: return + + val existingRequest = verifMutex.withLock { + outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } + } + + // As per spec: + // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. + if (existingRequest?.secretName == null) { + Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + // we don't need to cancel the request as we only request to one device + // just forget about the request now + verifMutex.withLock { + outgoingSecretRequests.remove(existingRequest) + } + + if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt deleted file mode 100644 index 69b405aedc..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -internal class SendGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val keyShareRequest: OutgoingRoomKeyRequest? = null, - val secretShareRequest: OutgoingSecretRequest? = null, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - val contentMap = MXUsersDevicesMap() - val eventType: String - val requestId: String - when { - params.keyShareRequest != null -> { - eventType = EventType.ROOM_KEY_REQUEST - requestId = params.keyShareRequest.requestId - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = credentials.deviceId, - requestId = params.keyShareRequest.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - body = params.keyShareRequest.requestBody - ) - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = System.currentTimeMillis() - }) - - params.keyShareRequest.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - } - params.secretShareRequest != null -> { - eventType = EventType.REQUEST_SECRET - requestId = params.secretShareRequest.requestId - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - requestId = params.secretShareRequest.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - secretName = params.secretShareRequest.secretName - ) - - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = System.currentTimeMillis() - }) - - params.secretShareRequest.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - } - else -> { - return buildErrorResult(params, "Unknown empty gossiping request").also { - Timber.e("Unknown empty gossiping request: $params") - } - } - } - try { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = eventType, - contentMap = contentMap, - transactionId = txnId - ) - ) - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt deleted file mode 100644 index fd472fe73b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -internal class SendGossipWorker( - context: Context, - params: WorkerParameters, - sessionManager: SessionManager -) : SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val secretValue: String, - val requestUserId: String?, - val requestDeviceId: String?, - val requestId: String?, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - @Inject lateinit var messageEncrypter: MessageEncrypter - @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - val eventType: String = EventType.SEND_SECRET - - val toDeviceContent = SecretSendEventContent( - requestId = params.requestId ?: "", - secretValue = params.secretValue - ) - - val requestingUserId = params.requestUserId ?: "" - val requestingDeviceId = params.requestDeviceId ?: "" - val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) - ?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.requestDeviceId}") - } - - val sendToDeviceMap = MXUsersDevicesMap() - - val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - return buildErrorResult(params, "no session with this device").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("no session with this device $requestingDeviceId, probably because there were no one-time keys.") - } - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to toDeviceContent.toContent() - ) - - try { - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) - } catch (failure: Throwable) { - Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") - } - - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = System.currentTimeMillis() - }) - - try { - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = EventType.ENCRYPTED, - contentMap = sendToDeviceMap, - transactionId = txnId - ) - ) - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.ACCEPTED - ) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index fffc2b4d4b..df29a494db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -137,10 +137,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature) isVerified = true } catch (e: Exception) { - Timber.tag(loggerTag.value).d(e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," + - " signature:$signature fingerprint:$fingerprint") - Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " + - " - signable json ${oneTimeKey.signalableJSONDictionary()}") + Timber.tag(loggerTag.value).d( + e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," + + " signature:$signature fingerprint:$fingerprint" + ) + Timber.tag(loggerTag.value).e( + "verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " + + " - signable json ${oneTimeKey.signalableJSONDictionary()}" + ) errorMessage = e.message } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index f9bcdf2c68..22c4e59b18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -17,22 +17,28 @@ package org.matrix.android.sdk.internal.crypto.actions import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO) + internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val cryptoStore: IMXCryptoStore) { + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, +) { /** * Import a list of megolm session keys. @@ -47,7 +53,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi fun handle(megolmSessionsData: List, fromBackup: Boolean, progressListener: ProgressListener?): ImportRoomKeysResult { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val totalNumbersOfKeys = megolmSessionsData.size var lastProgress = 0 @@ -62,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi if (null != decrypting) { try { val sessionId = megolmSessionData.sessionId - Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId") totalNumbersOfImportedKeys++ // cancel any outstanding room key requests for this session - val roomKeyRequestBody = RoomKeyRequestBody( - algorithm = megolmSessionData.algorithm, - roomId = megolmSessionData.roomId, - senderKey = megolmSessionData.senderKey, - sessionId = megolmSessionData.sessionId - ) - outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}") + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded( + megolmSessionData.sessionId ?: "", + megolmSessionData.roomId ?: "", + megolmSessionData.senderKey ?: "", + tryOrNull { + olmInboundGroupSessionWrappers + .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } + ?.firstKnownIndex?.toInt() + } ?: 0 + ) // Have another go at decrypting events sent with this session when (decrypting) { @@ -83,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi } } } catch (e: Exception) { - Timber.e(e, "## importRoomKeys() : onNewSession failed") + Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed") } } @@ -103,9 +113,9 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) } - val t1 = System.currentTimeMillis() + val t1 = clock.epochMillis() - Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 2ea4e1dd29..d555062442 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.crypto.algorithms import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService @@ -44,23 +42,4 @@ internal interface IMXDecrypting { * @param event the key event. */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} - - /** - * Determine if we have the keys necessary to respond to a room key request - * - * @param request keyRequest - * @return true if we have the keys and could (theoretically) share - */ - fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false - - /** - * Send the response to a room key request. - * - * @param request keyRequest - */ - fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} - - fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {} - - fun requestKeysForEvent(event: Event, withHeld: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt deleted file mode 100644 index 585bcdbbde..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent - -internal interface IMXWithHeldExtension { - fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 4c407c9eb9..d65b05f655 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,51 +17,32 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.session.StreamEventsManager import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) -internal class MXMegolmDecryption(private val userId: String, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val liveEventManager: Lazy -) : IMXDecrypting, IMXWithHeldExtension { +internal class MXMegolmDecryption( + private val olmDevice: MXOlmDevice, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val liveEventManager: Lazy +) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - // If cross signing is enabled, we don't send request until the keys are trusted - // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once - val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true - return decryptEvent(event, timeline, requestOnFail) + return decryptEvent(event, timeline, true) } @Throws(MXCryptoError::class) @@ -96,11 +74,13 @@ internal class MXMegolmDecryption(private val userId: String, } return runCatching { - olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, + olmDevice.decryptGroupMessage( + encryptedEventContent.ciphertext, event.roomId, timeline, encryptedEventContent.sessionId, - encryptedEventContent.senderKey) + encryptedEventContent.senderKey + ) } .fold( { olmDecryptionResult -> @@ -124,27 +104,27 @@ internal class MXMegolmDecryption(private val userId: String, if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // addEventToPendingList(event, timeline) - // The session might has been partially withheld (and only pass ratcheted) - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event, true) - } - // Encapsulate as withHeld exception - throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason) - } + // So we know that session, but it's ratcheted and we can't decrypt at that index if (requestKeysOnFail) { - requestKeysForEvent(event, false) + requestKeysForEvent(event) + } + // Check if partially withheld + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + // Encapsulate as withHeld exception + throw MXCryptoError.Base( + MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason + ) } throw MXCryptoError.Base( MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, "UNKNOWN_MESSAGE_INDEX", - null) + null + ) } val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) @@ -153,28 +133,26 @@ internal class MXMegolmDecryption(private val userId: String, throw MXCryptoError.Base( MXCryptoError.ErrorType.OLM, reason, - detailedReason) + detailedReason + ) } if (throwable is MXCryptoError.Base) { - if ( - /** if the session is unknown*/ - throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID - ) { + if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + // Check if it was withheld by sender to enrich error code val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { if (requestKeysOnFail) { - requestKeysForEvent(event, true) + requestKeysForEvent(event) } // Encapsulate as withHeld exception - throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + throw MXCryptoError.Base( + MXCryptoError.ErrorType.KEYS_WITHHELD, withHeldInfo.code?.value ?: "", withHeldInfo.reason) - } else { - // This is un-used in Matrix Android SDK2, not sure if needed - // addEventToPendingList(event, timeline) - if (requestKeysOnFail) { - requestKeysForEvent(event, false) - } + } + + if (requestKeysOnFail) { + requestKeysForEvent(event) } } } @@ -190,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - val sender = event.senderId ?: return - val encryptedEventContent = event.content.toModel() - val senderDevice = encryptedEventContent?.deviceId ?: return - - val recipients = if (event.senderId == userId || withHeld) { - mapOf( - userId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - userId to listOf("*"), - sender to listOf(senderDevice) - ) - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - - outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) + private fun requestKeysForEvent(event: Event) { + outgoingKeyRequestManager.requestKeyForEvent(event, false) } -// /** -// * Add an event to the list of those we couldn't decrypt the first time we -// * saw them. -// * -// * @param event the event to try to decrypt later -// * @param timelineId the timeline identifier -// */ -// private fun addEventToPendingList(event: Event, timelineId: String) { -// val encryptedEventContent = event.content.toModel() ?: return -// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" -// -// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } -// val events = timeline.getOrPut(timelineId) { ArrayList() } -// -// if (event !in events) { -// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") -// events.add(event) -// } -// } - /** * Handle a key event. * @@ -258,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String, return } if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + return + } Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") val forwardedRoomKeyContent = event.getClearContent().toModel() ?: return @@ -298,26 +236,57 @@ internal class MXMegolmDecryption(private val userId: String, } Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, + val addSessionResult = olmDevice.addInboundGroupSession( + roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, senderKey, forwardingCurve25519KeyChain, keysClaimed, - exportFormat) + exportFormat + ) - if (added) { + when (addSessionResult) { + is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex + is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex + else -> null + }?.let { index -> + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey -> + cryptoStore.getUserDeviceList(event.senderId ?: "") + ?.firstOrNull { + it.identityKey() == senderDeviceIdentityKey + } + }?.deviceId + + outgoingKeyRequestManager.onRoomKeyForwarded( + sessionId = roomKeyContent.sessionId, + algorithm = roomKeyContent.algorithm ?: "", + roomId = roomKeyContent.roomId, + senderKey = senderKey, + fromIndex = index, + fromDevice = fromDevice, + event = event) + + cryptoStore.saveIncomingForwardKeyAuditTrail( + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey, + algorithm = roomKeyContent.algorithm ?: "", + userId = event.senderId ?: "", + deviceId = fromDevice ?: "", + chainIndex = index.toLong()) + + // The index is used to decide if we cancel sent request or if we wait for a better key + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index) + } + } + + if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { + Timber.tag(loggerTag.value) + .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() - val content = RoomKeyRequestBody( - algorithm = roomKeyContent.algorithm, - roomId = roomKeyContent.roomId, - sessionId = roomKeyContent.sessionId, - senderKey = senderKey - ) - - outgoingGossipingRequestManager.cancelRoomKeyRequest(content) - onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) } } @@ -333,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String, Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") newSessionListener?.onNewSession(roomId, senderKey, sessionId) } - - override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { - val roomId = request.requestBody?.roomId ?: return false - val senderKey = request.requestBody.senderKey ?: return false - val sessionId = request.requestBody.sessionId ?: return false - return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) - } - - override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { - // sanity checks - if (request.requestBody == null) { - return - } - val userId = request.userId ?: return - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val body = request.requestBody - val sessionHolder = try { - olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body") - return@launch - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys() - } ?: return@launch Unit.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}") - } - - runCatching { deviceListManager.downloadKeys(listOf(userId), false) } - .mapCatching { - val deviceId = request.deviceId - val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") - if (deviceInfo == null) { - throw RuntimeException() - } else { - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.") - return@mapCatching - } - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - sendToDeviceTask.execute(sendToDeviceParams) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") - } - } - } - } - } - - override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.addWithHeldMegolmSession(withHeldInfo) - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 3eba04b9f1..096773a959 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -17,45 +17,24 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( - @UserId private val userId: String, private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, private val eventsManager: Lazy ) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - userId, olmDevice, - deviceListManager, - outgoingGossipingRequestManager, - messageEncrypter, - ensureOlmSessionsForDevicesAction, + outgoingKeyRequestManager, cryptoStore, - sendToDeviceTask, - coroutineDispatchers, - cryptoCoroutineScope, eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index f052194230..061c349645 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.forEach import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode @@ -46,6 +45,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO) @@ -64,7 +64,8 @@ internal class MXMegolmEncryption( private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, ) : IMXEncrypting, IMXGroupEncryption { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note @@ -86,11 +87,11 @@ internal class MXMegolmEncryption( override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { - val ts = System.currentTimeMillis() + val ts = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") - Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") + Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -99,7 +100,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis") } } @@ -125,14 +126,14 @@ internal class MXMegolmEncryption( } override suspend fun preshareKey(userIds: List) { - val ts = System.currentTimeMillis() + val ts = clock.epochMillis() Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...") val devices = getDevicesInRoom(userIds) val outboundSession = ensureOutboundSession(devices.allowedDevices) notifyWithheldForSession(devices.withHeldDevices, outboundSession) - Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis") } /** @@ -148,12 +149,14 @@ internal class MXMegolmEncryption( "ed25519" to olmDevice.deviceEd25519Key!! ) - olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, - emptyList(), keysClaimedMap, false) + olmDevice.addInboundGroupSession( + sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, + emptyList(), keysClaimedMap, false + ) defaultKeysBackupService.maybeBackupKeys() - return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore)) + return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock) } /** @@ -243,12 +246,12 @@ internal class MXMegolmEncryption( payload["type"] = EventType.ROOM_KEY payload["content"] = submap - var t0 = System.currentTimeMillis() + var t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) Timber.tag(loggerTag.value).v( - """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms""" .trimMargin() ) val contentMap = MXUsersDevicesMap() @@ -281,27 +284,16 @@ internal class MXMegolmEncryption( // attempted to share with) rather than the contentMap (those we did // share with), because we don't want to try to claim a one-time-key // for dead devices on every message. - val gossipingEventBuffer = arrayListOf() - for ((userId, devicesToShareWith) in devicesByUser) { + for ((_, devicesToShareWith) in devicesByUser) { for (deviceInfo in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) - gossipingEventBuffer.add( - Event( - type = EventType.ROOM_KEY, - senderId = myUserId, - content = submap.apply { - this["session_key"] = "" - // we add a fake key for trail - this["_dest"] = "$userId|${deviceInfo.deviceId}" - } - )) + // XXX is it needed to add it to the audit trail? + // For now decided that no, we are more interested by forward trail } } - cryptoStore.saveGossipingEvents(gossipingEventBuffer) - if (haveTargets) { - t0 = System.currentTimeMillis() + t0 = clock.epochMillis() Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) @@ -309,7 +301,7 @@ internal class MXMegolmEncryption( withContext(coroutineDispatchers.io) { sendToDeviceTask.execute(sendToDeviceParams) } - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>") @@ -333,14 +325,17 @@ internal class MXMegolmEncryption( sessionId: String, senderKey: String?, code: WithHeldCode) { - Timber.tag(loggerTag.value).d("notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + - " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}") + Timber.tag(loggerTag.value).d( + "notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + + " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}" + ) val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, algorithm = MXCRYPTO_ALGORITHM_MEGOLM, sessionId = sessionId, - codeString = code.value + codeString = code.value, + fromDevice = myDeviceId ) val params = SendToDeviceTask.Params( EventType.ROOM_KEY_WITHHELD, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 136fdc05f5..4225d604aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( @@ -42,7 +43,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope) { + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, +) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( @@ -58,7 +61,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor( messageEncrypter = messageEncrypter, warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, coroutineDispatchers = coroutineDispatchers, - cryptoCoroutineScope = cryptoCoroutineScope + cryptoCoroutineScope = cryptoCoroutineScope, + clock = clock, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 091abd4974..28d925d8fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -18,21 +18,24 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber internal class MXOutboundSessionInfo( // The id of the session val sessionId: String, val sharedWithHelper: SharedWithHelper, + private val clock: Clock, // When the session was created - private val creationTime: Long = System.currentTimeMillis()) { + private val creationTime: Long = clock.epochMillis(), +) { // Number of times this session has been used var useCount: Int = 0 fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { var needsRotation = false - val sessionLifetime = System.currentTimeMillis() - creationTime + val sessionLifetime = clock.epochMillis() - creationTime if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt index 59d78c3e05..61ad345c62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt @@ -36,6 +36,7 @@ internal class SharedWithHelper( userId = deviceInfo.userId, deviceId = deviceInfo.deviceId, deviceIdentityKey = deviceInfo.identityKey() ?: "", - chainIndex = chainIndex) + chainIndex = chainIndex + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 0db8700852..1e66fe84c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -45,20 +45,26 @@ internal class MXOlmDecryption( override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val olmEventContent = event.content.toModel() ?: run { Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, - MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.BAD_EVENT_FORMAT, + MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON + ) } val cipherText = olmEventContent.ciphertext ?: run { Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON + ) } val senderKey = olmEventContent.senderKey ?: run { Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, - MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.MISSING_SENDER_KEY, + MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON + ) } val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { @@ -98,52 +104,70 @@ internal class MXOlmDecryption( } if (olmPayloadContent.recipient != userId) { - Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" + - " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, - String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) + Timber.tag(loggerTag.value).e( + "## decryptEvent() : Event ${event.eventId}:" + + " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId" + ) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.BAD_RECIPIENT, + String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient) + ) } val recipientKeys = olmPayloadContent.recipientKeys ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + - " property; cannot prevent unknown-key attack") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) + Timber.tag(loggerTag.value).e( + "## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + + " property; cannot prevent unknown-key attack" + ) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys") + ) } val ed25519 = recipientKeys["ed25519"] if (ed25519 != olmDevice.deviceEd25519Key) { Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, - MXCryptoError.BAD_RECIPIENT_KEY_REASON) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, + MXCryptoError.BAD_RECIPIENT_KEY_REASON + ) } if (olmPayloadContent.sender.isNullOrBlank()) { Timber.tag(loggerTag.value) .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender") + ) } if (olmPayloadContent.sender != event.senderId) { Timber.tag(loggerTag.value) .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") - throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, - String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.FORWARDED_MESSAGE, + String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender) + ) } if (olmPayloadContent.roomId != event.roomId) { Timber.tag(loggerTag.value) .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, - String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.BAD_ROOM, + String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId) + ) } val keys = olmPayloadContent.keys ?: run { Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON) + throw MXCryptoError.Base( + MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON + ) } return MXEventDecryptionResult( @@ -241,8 +265,4 @@ internal class MXOlmDecryption( return res["payload"] } - - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - // nop - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt index 972176e25b..d5c5e85e41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt @@ -26,6 +26,7 @@ internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: fun create(): MXOlmDecryption { return MXOlmDecryption( olmDevice, - userId) + userId + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt index 50ce2d2bf3..44e55900e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt @@ -38,6 +38,7 @@ internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: cryptoStore, messageEncrypter, deviceListManager, - ensureOlmSessionsForUsersAction) + ensureOlmSessionsForUsersAction + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 91b6af6fc3..65bbb0c412 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey import org.matrix.android.sdk.internal.util.base64ToBase64Url import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 import org.matrix.android.sdk.internal.util.base64UrlToBase64 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File @@ -42,8 +43,9 @@ internal object MXEncryptedAttachments { fun encrypt(clearStream: InputStream, outputFile: File, + clock: Clock, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo { - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() val secureRandom = SecureRandom() val initVectorBytes = ByteArray(16) { 0.toByte() } @@ -100,7 +102,7 @@ internal object MXEncryptedAttachments { hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), v = "v2" ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + .also { Timber.v("Encrypt in ${clock.epochMillis() - t0}ms") } } // fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair { @@ -159,8 +161,8 @@ internal object MXEncryptedAttachments { * @param attachmentStream the attachment stream. Will be closed after this method call. * @return the encryption file info */ - fun encryptAttachment(attachmentStream: InputStream): EncryptionResult { - val t0 = System.currentTimeMillis() + fun encryptAttachment(attachmentStream: InputStream, clock: Clock): EncryptionResult { + val t0 = clock.epochMillis() val secureRandom = SecureRandom() // generate a random iv key @@ -221,7 +223,7 @@ internal object MXEncryptedAttachments { ), encryptedByteArray = byteArrayOutputStream.toByteArray() ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + .also { Timber.v("Encrypt in ${clock.epochMillis() - t0}ms") } } /** @@ -234,14 +236,16 @@ internal object MXEncryptedAttachments { */ fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, - outputStream: OutputStream): Boolean { + outputStream: OutputStream, + clock: Clock + ): Boolean { // sanity checks if (null == attachmentStream || elementToDecrypt == null) { Timber.e("## decryptAttachment() : null stream") return false } - val t0 = System.currentTimeMillis() + val t0 = clock.epochMillis() try { val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) @@ -279,7 +283,8 @@ internal object MXEncryptedAttachments { return false } - return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + Timber.v("Decrypt in ${clock.epochMillis() - t0} ms") + return true } catch (oom: OutOfMemoryError) { Timber.e(oom, "## decryptAttachment() failed: OOM") } catch (e: Exception) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index ba1718688f..b8cdc922cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask @@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, private val workManagerProvider: WorkManagerProvider, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository ) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { @@ -566,8 +568,10 @@ internal class DefaultCrossSigningService @Inject constructor( } // Sign the other MasterKey with our UserSigning key - val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, - otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + val newSignature = JsonCanonicalizer.getCanonicalJson( + Map::class.java, + otherMasterKeys.signalableJSONDictionary() + ).let { userPkSigning?.sign(it) } if (newSignature == null) { // race?? @@ -684,7 +688,8 @@ internal class DefaultCrossSigningService @Inject constructor( val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") ?: return legacyFallbackTrust( locallyTrusted, - DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey() + DeviceTrustResult.MissingDeviceSignature( + otherDeviceId, otherKeys.selfSigningKey() ?.unpaddedBase64PublicKey ?: "" ) @@ -733,7 +738,8 @@ internal class DefaultCrossSigningService @Inject constructor( val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") ?: return legacyFallbackTrust( locallyTrusted, - DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey() + DeviceTrustResult.MissingDeviceSignature( + otherDevice.deviceId, otherKeys.selfSigningKey() ?.unpaddedBase64PublicKey ?: "" ) @@ -781,7 +787,8 @@ internal class DefaultCrossSigningService @Inject constructor( // If it's me, recheck trust of all users and devices? val users = ArrayList() if (otherUserId == userId && currentTrust != trusted) { -// reRequestAllPendingRoomKeyRequest() + // notify key requester + outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { users.add(it) checkUserTrust(it).isVerified() @@ -796,19 +803,4 @@ internal class DefaultCrossSigningService @Inject constructor( } } } - -// private fun reRequestAllPendingRoomKeyRequest() { -// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") -// cryptoStore.getOutgoingRoomKeyRequests().forEach { -// it.requestBody?.let { requestBody -> -// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { -// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) -// } else { -// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) -// } -// } -// } -// } -// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index e63a6dc791..d2dae1c112 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -63,16 +63,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBack import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 @@ -112,16 +107,11 @@ internal class DefaultKeysBackupService @Inject constructor( // Tasks private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val deleteBackupTask: DeleteBackupTask, - private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask, - private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, - private val deleteSessionDataTask: DeleteSessionsDataTask, private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, private val getKeysBackupVersionTask: GetKeysBackupVersionTask, private val getRoomSessionDataTask: GetRoomSessionDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getSessionsDataTask: GetSessionsDataTask, - private val storeRoomSessionDataTask: StoreRoomSessionDataTask, - private val storeSessionsDataTask: StoreRoomSessionsDataTask, private val storeSessionDataTask: StoreSessionsDataTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor @@ -168,58 +158,63 @@ internal class DefaultKeysBackupService @Inject constructor( override fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - withContext(coroutineDispatchers.crypto) { - val olmPkDecryption = OlmPkDecryption() - val signalableMegolmBackupAuthData = if (password != null) { - // Generate a private key from the password - val backgroundProgressListener = if (progressListener == null) { - null - } else { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - try { - progressListener.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "prepareKeysBackupVersion: onProgress failure") - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + try { + val olmPkDecryption = OlmPkDecryption() + val signalableMegolmBackupAuthData = if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") } } } } - - val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) - SignalableMegolmBackupAuthData( - publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), - privateKeySalt = generatePrivateKeyResult.salt, - privateKeyIterations = generatePrivateKeyResult.iterations - ) - } else { - val publicKey = olmPkDecryption.generateKey() - - SignalableMegolmBackupAuthData( - publicKey = publicKey - ) } - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) - - val signedMegolmBackupAuthData = MegolmBackupAuthData( - publicKey = signalableMegolmBackupAuthData.publicKey, - privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, - privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, - signatures = objectSigner.signObject(canonicalJson) + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + SignalableMegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations ) + } else { + val publicKey = olmPkDecryption.generateKey() - MegolmBackupCreationInfo( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, - authData = signedMegolmBackupAuthData, - recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + SignalableMegolmBackupAuthData( + publicKey = publicKey ) } - }.foldToCallback(callback) + + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) + + val signedMegolmBackupAuthData = MegolmBackupAuthData( + publicKey = signalableMegolmBackupAuthData.publicKey, + privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, + privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, + signatures = objectSigner.signObject(canonicalJson) + ) + + val creationInfo = MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) + uiHandler.post { + callback.onSuccess(creationInfo) + } + } catch (failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } } } @@ -267,41 +262,39 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun deleteBackup(version: String, callback: MatrixCallback?) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeysBackupVersion so this is symmetrical). - if (keysBackupVersion != null && version == keysBackupVersion?.version) { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Unknown - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (keysBackupVersion != null && version == keysBackupVersion?.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } - deleteBackupTask - .configureWith(DeleteBackupTask.Params(version)) { - this.callback = object : MatrixCallback { - private fun eventuallyRestartBackup() { - // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (state == KeysBackupState.Unknown) { - checkAndStartKeysBackup() - } - } - - override fun onSuccess(data: Unit) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onSuccess(Unit) } - } - - override fun onFailure(failure: Throwable) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onFailure(failure) } + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() } } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } + } } - .executeBy(taskExecutor) - } + } + .executeBy(taskExecutor) } } @@ -480,10 +473,11 @@ internal class DefaultKeysBackupService @Inject constructor( if (authData == null) { Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() @@ -516,7 +510,8 @@ internal class DefaultKeysBackupService @Inject constructor( UpdateKeysBackupVersionBody( algorithm = keysBackupVersion.algorithm, authData = newMegolmBackupAuthDataWithNewSignature.toJsonDict(), - version = keysBackupVersion.version) + version = keysBackupVersion.version + ) } // And send it to the homeserver @@ -535,11 +530,15 @@ internal class DefaultKeysBackupService @Inject constructor( checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - callback.onSuccess(data) + uiHandler.post { + callback.onSuccess(data) + } } override fun onFailure(failure: Throwable) { - callback.onFailure(failure) + uiHandler.post { + callback.onFailure(failure) + } } } } @@ -553,15 +552,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val isValid = withContext(coroutineDispatchers.crypto) { - isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) if (!isValid) { Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") - - callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } } else { trustKeysBackupVersion(keysBackupVersion, true, callback) } @@ -573,15 +571,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val recoveryKey = withContext(coroutineDispatchers.crypto) { - recoveryKeyFromPassword(password, keysBackupVersion, null) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) if (recoveryKey == null) { Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { // Check trust using the recovery key trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) @@ -592,30 +589,28 @@ internal class DefaultKeysBackupService @Inject constructor( override fun onSecretKeyGossip(secret: String) { Timber.i("## CrossSigning - onSecretKeyGossip") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { try { - when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) { - KeysBackupLastVersionResult.NoKeysBackup -> { - Timber.d("No keys backup found") - } - is KeysBackupLastVersionResult.KeysBackup -> { - val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult - val recoveryKey = computeRecoveryKey(secret.fromBase64()) - if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - awaitCallback { - trustKeysBackupVersion(keysBackupVersion, true, it) - } - val importResult = awaitCallback { - restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) - } - withContext(coroutineDispatchers.crypto) { - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) - } - Timber.i("onSecretKeyGossip: Recovered keys $importResult") - } else { - Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult() + ?: return@launch Unit.also { + Timber.d("Failed to get backup last version") } + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback { + trustKeysBackupVersion(keysBackupVersion, true, it) } + // we don't want to start immediately downloading all as it can take very long + +// val importResult = awaitCallback { +// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) +// } + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } + Timber.i("onSecretKeyGossip: saved valid backup key") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") } } catch (failure: Throwable) { Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") @@ -678,9 +673,9 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { - val decryption = withContext(coroutineDispatchers.crypto) { + val decryption = withContext(coroutineDispatchers.computation) { // Check if the recovery is valid before going any further if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") @@ -719,14 +714,18 @@ internal class DefaultKeysBackupService @Inject constructor( } } } - Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + - " of $sessionsFromHsCount from the backup store on the homeserver") + Timber.v( + "restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + + " of $sessionsFromHsCount from the backup store on the homeserver" + ) // Do not trigger a backup for them if they come from the backup version we are using val backUp = keysVersionResult.version != keysBackupVersion?.version if (backUp) { - Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" + - " to backup version: ${keysBackupVersion?.version}") + Timber.v( + "restoreKeysWithRecoveryKey: Those keys will be backed up" + + " to backup version: ${keysBackupVersion?.version}" + ) } // Import them into the crypto store @@ -749,7 +748,19 @@ internal class DefaultKeysBackupService @Inject constructor( } result } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -761,7 +772,7 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { val progressListener = if (stepProgressListener != null) { object : ProgressListener { @@ -786,7 +797,19 @@ internal class DefaultKeysBackupService @Inject constructor( restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) } } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -801,19 +824,27 @@ internal class DefaultKeysBackupService @Inject constructor( // Get key for the room and for the session val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) // Convert to KeysBackupData - KeysBackupData(mutableMapOf( - roomId to RoomKeysBackupData(mutableMapOf( - sessionId to data - )) - )) + KeysBackupData( + mutableMapOf( + roomId to RoomKeysBackupData( + mutableMapOf( + sessionId to data + ) + ) + ) + ) } else if (roomId != null) { // Get all keys for the room - val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + val data = withContext(coroutineDispatchers.io) { + getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + } // Convert to KeysBackupData KeysBackupData(mutableMapOf(roomId to data)) } else { // Get all keys - getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + withContext(coroutineDispatchers.io) { + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } } } @@ -1326,7 +1357,8 @@ internal class DefaultKeysBackupService @Inject constructor( "sender_key" to sessionData.senderKey, "sender_claimed_keys" to sessionData.senderClaimedKeys, "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), - "session_key" to sessionData.sessionKey) + "session_key" to sessionData.sessionKey + ) val json = MoshiProvider.providesMoshi() .adapter(Map::class.java) @@ -1354,7 +1386,8 @@ internal class DefaultKeysBackupService @Inject constructor( sessionData = mapOf( "ciphertext" to encryptedSessionBackupData.mCipherText, "mac" to encryptedSessionBackupData.mMac, - "ephemeral" to encryptedSessionBackupData.mEphemeralKey) + "ephemeral" to encryptedSessionBackupData.mEphemeralKey + ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt index c12879dbee..4d5b38acbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -26,6 +26,7 @@ import java.util.UUID import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.experimental.xor +import kotlin.system.measureTimeMillis private const val SALT_LENGTH = 32 private const val DEFAULT_ITERATION = 500_000 @@ -91,52 +92,53 @@ internal fun deriveKey(password: String, iterations: Int, progressListener: ProgressListener?): ByteArray { // Note: copied and adapted from MXMegolmExportEncryption - val t0 = System.currentTimeMillis() - // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. // noticed as dklen/hlen - // dklen = 256 - // hlen = 512 - val prf = Mac.getInstance("HmacSHA512") - - prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) - // 256 bits key length val dk = ByteArray(32) - val uc = ByteArray(64) - // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen - prf.update(salt.toByteArray()) - val int32BE = byteArrayOf(0, 0, 0, 1) - prf.update(int32BE) - prf.doFinal(uc, 0) + measureTimeMillis { + // dklen = 256 + // hlen = 512 + val prf = Mac.getInstance("HmacSHA512") - // copy to the key - System.arraycopy(uc, 0, dk, 0, dk.size) + prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) - var lastProgress = -1 + val uc = ByteArray(64) - for (index in 2..iterations) { - // Uc = PRF(Password, Uc-1) - prf.update(uc) + // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen + prf.update(salt.toByteArray()) + val int32BE = byteArrayOf(0, 0, 0, 1) + prf.update(int32BE) prf.doFinal(uc, 0) - // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in dk.indices) { - dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] - } + // copy to the key + System.arraycopy(uc, 0, dk, 0, dk.size) - val progress = (index + 1) * 100 / iterations - if (progress != lastProgress) { - lastProgress = progress - progressListener?.onProgress(lastProgress, 100) + var lastProgress = -1 + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in dk.indices) { + dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] + } + + val progress = (index + 1) * 100 / iterations + if (progress != lastProgress) { + lastProgress = progress + progressListener?.onProgress(lastProgress, 100) + } } + }.also { + Timber.v("KeysBackupPassword: deriveKeys() : $iterations in $it ms") } - Timber.v("KeysBackupPassword: deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") - return dk } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt index 7f1b03b932..f90522b036 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -40,7 +40,8 @@ internal class DefaultDeleteRoomSessionDataTask @Inject constructor( roomKeysApi.deleteRoomSessionData( params.roomId, params.sessionId, - params.version) + params.version + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt index 394cc861d6..7db33e0c46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -38,7 +38,8 @@ internal class DefaultDeleteRoomSessionsDataTask @Inject constructor( return executeRequest(globalErrorReceiver) { roomKeysApi.deleteRoomSessionsData( params.roomId, - params.version) + params.version + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt index ff515ed80f..df8e6bfebf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -41,7 +41,8 @@ internal class DefaultGetRoomSessionDataTask @Inject constructor( roomKeysApi.getRoomSessionData( params.roomId, params.sessionId, - params.version) + params.version + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt index 1b4fe2d966..f9ea078ef2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -39,7 +39,8 @@ internal class DefaultGetRoomSessionsDataTask @Inject constructor( return executeRequest(globalErrorReceiver) { roomKeysApi.getRoomSessionsData( params.roomId, - params.version) + params.version + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt index 180aaecf82..637c6ddeb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -44,7 +44,8 @@ internal class DefaultStoreRoomSessionDataTask @Inject constructor( params.roomId, params.sessionId, params.version, - params.keyBackupData) + params.keyBackupData + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt index d1aa9d2eb0..09e52da755 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -42,7 +42,8 @@ internal class DefaultStoreRoomSessionsDataTask @Inject constructor( roomKeysApi.storeRoomSessionsData( params.roomId, params.version, - params.roomKeysBackupData) + params.roomKeysBackupData + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt index 3dbeafe9de..47f2578c43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -40,7 +40,8 @@ internal class DefaultStoreSessionsDataTask @Inject constructor( return executeRequest(globalErrorReceiver) { roomKeysApi.storeSessionsData( params.version, - params.keysBackupData) + params.keysBackupData + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt index 927d049eca..d7ce553f39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -34,7 +34,7 @@ internal data class OlmSessionWrapper( /** * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` */ - fun onMessageReceived() { - lastReceivedMessageTs = System.currentTimeMillis() + fun onMessageReceived(currentTimeMillis: Long) { + lastReceivedMessageTs = currentTimeMillis } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 8c877593e7..1ded9c6c7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption @@ -57,7 +57,7 @@ import kotlin.experimental.and internal class DefaultSharedSecretStorageService @Inject constructor( @UserId private val userId: String, private val accountDataService: SessionAccountDataService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val secretShareManager: SecretShareManager, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { @@ -213,7 +213,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( secretKey.privateKey, ByteArray(32) { 0.toByte() }, secretName.toByteArray(), - 64) + 64 + ) // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key val aesKey = pseudoRandomKey.copyOfRange(0, 32) @@ -255,7 +256,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( secretKey.privateKey, ByteArray(32) { 0.toByte() }, secretName.toByteArray(), - 64) + 64 + ) // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key val aesKey = pseudoRandomKey.copyOfRange(0, 32) @@ -378,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return IntegrityResult.Success(keyInfo.content.passphrase != null) } - override fun requestSecret(name: String, myOtherDeviceId: String) { - outgoingGossipingRequestManager.sendSecretShareRequest( - name, - mapOf(userId to listOf(myOtherDeviceId)) - ) + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { + secretShareManager.requestSecretTo(myOtherDeviceId, name) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 8bedb78808..d720777ae1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper @@ -82,6 +81,15 @@ internal interface IMXCryptoStore { */ fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + /** + * Enable or disable key gossiping. + * Default is true. + * If set to false this device won't send key_request nor will accept key forwarded + */ + fun enableKeyGossiping(enable: Boolean) + + fun isKeyGossipingEnabled(): Boolean + /** * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. * @@ -125,18 +133,6 @@ internal interface IMXCryptoStore { */ fun getDeviceTrackingStatuses(): Map - /** - * @return the pending IncomingRoomKeyRequest requests - */ - fun getPendingIncomingRoomKeyRequests(): List - - fun getPendingIncomingGossipingRequests(): List - - fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) - - fun storeIncomingGossipingRequests(requests: List) -// fun getPendingIncomingSecretShareRequests(): List - /** * Indicate if the store contains data for the passed account. * @@ -377,7 +373,9 @@ internal interface IMXCryptoStore { * @param requestBody the request body * @return an OutgoingRoomKeyRequest instance or null */ - fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List /** * Look for an existing outgoing room key request, and if none is found, add a new one. @@ -385,39 +383,59 @@ internal interface IMXCryptoStore { * @param request the request * @return either the same instance as passed in, or the existing one. */ - fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest + fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) + fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) + fun updateOutgoingRoomKeyReply( + roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) - fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? + fun deleteOutgoingRoomKeyRequest(requestId: String) + fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) - fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event)) + fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String + ) - fun saveGossipingEvents(events: List) + fun saveWithheldAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String + ) - fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { - updateGossipingRequestState( - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - state = state - ) - } + fun saveForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) - fun updateGossipingRequestState(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) - - /** - * Search an IncomingRoomKeyRequest - * - * @param userId the user id - * @param deviceId the device id - * @param requestId the request id - * @return an IncomingRoomKeyRequest if it exists, else null - */ - fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? - - fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + fun saveIncomingForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) fun addNewSessionListener(listener: NewSessionListener) @@ -477,17 +495,15 @@ internal interface IMXCryptoStore { fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap // Dev tools - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> - fun getOutgoingSecretKeyRequests(): List - fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? - fun getIncomingRoomKeyRequests(): List - fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEvents(): List + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun logDbUsageInfo() + fun getOutgoingRoomKeyRequests(inStates: Set): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index fad901f95d..d5750a2e2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration import io.realm.Sort +import io.realm.kotlin.createObject import io.realm.kotlin.where import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper @@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity @@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.DeviceId @@ -97,6 +95,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmOutboundGroupSession @@ -105,12 +104,15 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject +private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) + @SessionScope internal class RealmCryptoStore @Inject constructor( @CryptoDatabase private val realmConfiguration: RealmConfiguration, private val crossSigningKeysMapper: CrossSigningKeysMapper, @UserId private val userId: String, - @DeviceId private val deviceId: String? + @DeviceId private val deviceId: String?, + private val clock: Clock, ) : IMXCryptoStore { /* ========================================================================================== @@ -271,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) - .findFirst() - ?.let { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) + .findAll() + .mapNotNull { CryptoMapper.mapToModel(it) } + .firstOrNull { + it.identityKey() == identityKey } } } @@ -307,7 +310,7 @@ internal class RealmCryptoStore @Inject constructor( // Add the device Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = System.currentTimeMillis() + newEntity.firstTimeSeenLocalTs = clock.epochMillis() userEntity.devices.add(newEntity) } else { // Update the device @@ -741,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - senderKey = session.senderKey - putInboundGroupSession(session) - } + val existing = realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() - realm.insertOrUpdate(realmOlmInboundGroupSession) + if (existing != null) { + // we want to keep the existing backup status + existing.putInboundGroupSession(session) + } else { + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.senderKey + putInboundGroupSession(session) + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } } } @@ -792,7 +804,7 @@ internal class RealmCryptoStore @Inject constructor( if (outboundGroupSession != null) { val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { - creationTime = System.currentTimeMillis() + creationTime = clock.epochMillis() putOutboundGroupSession(outboundGroupSession) } entity.outboundSessionInfo = info @@ -877,17 +889,33 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { + doRealmTransaction(realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { + val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() val key = OlmInboundGroupSessionEntity.createPrimaryKey( - olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), - olmInboundGroupSessionWrapper.senderKey) + sessionIdentifier, + olmInboundGroupSessionWrapper.senderKey + ) - it.where() + val existing = realm.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() - ?.backedUp = true + + if (existing != null) { + existing.backedUp = true + } else { + // ... might be in cache but not yet persisted, create a record to persist backedup state + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = olmInboundGroupSessionWrapper.senderKey + putInboundGroupSession(olmInboundGroupSessionWrapper) + backedUp = true + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } catch (e: OlmException) { Timber.e(e, "OlmException") } @@ -926,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun enableKeyGossiping(enable: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping = enable + } + } + + override fun isKeyGossipingEnabled(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping + } ?: true + } + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { return doWithRealm(realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices @@ -1007,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor( ?: defaultValue } - override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.roomId == requestBody.roomId && @@ -1021,87 +1062,118 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) - .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull() } - override fun getIncomingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List { + // TODO this annoying we have to load all return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + }.map { + it.toOutgoingKeyRequest() + }.filter { + it.requestBody?.algorithm == algorithm && + it.requestBody?.senderKey == senderKey } } - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + override fun getGossipingEventsTrail(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING) + realm.where().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } val dataSourceFactory = realmDataSourceFactory.map { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest - ?: IncomingRoomKeyRequest( - requestBody = null, - deviceId = "", - userId = "", - requestId = "", - state = GossipingRequestState.NONE, - localCreationTimestamp = 0 - ) + AuditTrailMapper.map(it) + // mm we can't map not null... + ?: createUnknownTrail() } - return monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) } - override fun getGossipingEventsTrail(): LiveData> { + private fun createUnknownTrail() = AuditTrail( + clock.epochMillis(), + TrailType.Unknown, + IncomingKeyRequestInfo( + "", + "", + "", + "", + "", + "", + "", + ) + ) + + override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) + realm.where() + .equalTo(AuditTrailEntityFields.TYPE, type.name) + .sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } - val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } - val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + val dataSourceFactory = realmDataSourceFactory.map { entity -> + (AuditTrailMapper.map(entity) + // mm we can't map not null... + ?: createUnknownTrail() + ).let { mapper.invoke(it) } + } + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) - return trail } - override fun getGossipingEvents(): List { + override fun getGossipingEvents(): List { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - }.map { - it.toModel() + realm.where() + }.mapNotNull { + AuditTrailMapper.map(it) } } - override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, + recipients: Map>, + fromIndex: Int): OutgoingKeyRequest { // Insert the request and return the one passed in parameter - var request: OutgoingRoomKeyRequest? = null + lateinit var request: OutgoingKeyRequest doRealmTransaction(realmConfiguration) { realm -> - val existing = realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + val existing = realm.where() + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) .findAll() - .mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - }.firstOrNull { + .map { + it.toOutgoingKeyRequest() + }.also { + if (it.size > 1) { + // there should be one or zero but not more, worth warning + Timber.tag(loggerTag.value).w("There should not be more than one active key request per session") + } + } + .firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.sessionId == requestBody.sessionId && it.requestBody?.senderKey == requestBody.senderKey && @@ -1109,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor( } if (existing == null) { - request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply { this.requestId = RequestIdHelper.createUniqueRequestId() this.setRecipients(recipients) - this.requestState = OutgoingGossipingRequestState.UNSENT - this.type = GossipRequestType.KEY - this.requestedInfoStr = requestBody.toJson() - }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + this.requestedIndex = fromIndex + this.requestState = OutgoingRoomKeyRequestState.UNSENT + this.setRequestBody(requestBody) + this.creationTimeStamp = clock.epochMillis() + }.toOutgoingKeyRequest() } else { request = existing } @@ -1123,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? { - var request: OutgoingSecretRequest? = null - - // Insert the request and return the one passed in parameter + override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { doRealmTransaction(realmConfiguration) { realm -> - val existing = realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) - .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) - .findAll() - .mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingSecretRequest - }.firstOrNull() - if (existing == null) { - request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { - this.type = GossipRequestType.SECRET - setRecipients(recipients) - this.requestState = OutgoingGossipingRequestState.UNSENT - this.requestId = RequestIdHelper.createUniqueRequestId() - this.requestedInfoStr = secretName - }.toOutgoingGossipingRequest() as? OutgoingSecretRequest - } else { - request = existing - } - } - - return request - } - - override fun saveGossipingEvents(events: List) { - monarchy.writeAsync { realm -> - val now = System.currentTimeMillis() - events.forEach { event -> - val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now - val entity = GossipingEventEntity( - type = event.type, - sender = event.senderId, - ageLocalTs = ageLocalTs, - content = ContentMapper.map(event.content) - ).apply { - sendState = SendState.SYNCED - decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) - decryptionErrorCode = event.mCryptoError?.name - } - realm.insertOrUpdate(entity) - } - } - } - -// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { -// val statesIndex = states.map { it.ordinal }.toTypedArray() -// return doRealmQueryAndCopy(realmConfiguration) { realm -> -// realm.where() -// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) -// .findAll() -// .filter {entity -> -// states.any { it == entity.requestState} -// } -// }.mapNotNull { -// ContentMapper.map(it.content)?.toModel() -// } -// ?.toOutgoingRoomKeyRequest() -// } -// -// override fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? { -// val statesIndex = states.map { it.ordinal }.toTypedArray() -// return doRealmQueryAndCopy(realmConfiguration) { -// it.where() -// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) -// .findFirst() -// } -// ?.toOutgoingSecretRequest() -// } - -// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { -// doRealmTransaction(realmConfiguration) { -// val obj = OutgoingRoomKeyRequestEntity().apply { -// requestId = request.requestId -// cancellationTxnId = request.cancellationTxnId -// state = request.state.ordinal -// putRecipients(request.recipients) -// putRequestBody(request.requestBody) -// } -// -// it.insertOrUpdate(obj) -// } -// } - -// override fun deleteOutgoingRoomKeyRequest(transactionId: String) { -// doRealmTransaction(realmConfiguration) { -// it.where() -// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) -// .findFirst() -// ?.deleteFromRealm() -// } -// } - -// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { -// if (incomingRoomKeyRequest == null) { -// return -// } -// -// doRealmTransaction(realmConfiguration) { -// // Delete any previous store request with the same parameters -// it.where() -// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) -// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) -// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) -// .findAll() -// .deleteAllFromRealm() -// -// // Then store it -// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { -// userId = incomingRoomKeyRequest.userId -// deviceId = incomingRoomKeyRequest.deviceId -// requestId = incomingRoomKeyRequest.requestId -// putRequestBody(incomingRoomKeyRequest.requestBody) -// } -// } -// } - -// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { -// doRealmTransaction(realmConfiguration) { -// it.where() -// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) -// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) -// .findAll() -// .filter { -// ContentMapper.map(it.content).toModel()?.let { -// -// } -// } -// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) -// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) -// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) -// // .findAll() -// // .deleteAllFromRealm() -// } -// } - - override fun updateGossipingRequestState(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) { - doRealmTransaction(realmConfiguration) { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, requestUserId) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, requestDeviceId) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, requestId) - .findAll().forEach { - it.requestState = state - } - } - } - - override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { - doRealmTransaction(realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) - .findAll().forEach { - it.requestState = state - } - } - } - - override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) - .findAll() - .mapNotNull { entity -> - entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest - } - .firstOrNull() - } - } - - override fun getPendingIncomingRoomKeyRequests(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) - .findAll() - .map { entity -> - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } - } - } - - override fun getPendingIncomingGossipingRequests(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) - .findAll() - .mapNotNull { entity -> - when (entity.type) { - GossipRequestType.KEY -> { - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } - GossipRequestType.SECRET -> { - IncomingSecretShareRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - secretName = entity.getRequestedSecretName(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestState = newState + if (newState == OutgoingRoomKeyRequestState.UNSENT) { + // clear the old replies + this.replies.deleteAllFromRealm() } } } } - override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { - doRealmTransactionAsync(realmConfiguration) { realm -> - - // After a clear cache, we might have a - - realm.createObject(IncomingGossipingRequestEntity::class.java).let { - it.otherDeviceId = request.deviceId - it.otherUserId = request.userId - it.requestId = request.requestId ?: "" - it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis() - if (request is IncomingSecretShareRequest) { - it.type = GossipRequestType.SECRET - it.requestedInfoStr = request.secretName - } else if (request is IncomingRoomKeyRequest) { - it.type = GossipRequestType.KEY - it.requestedInfoStr = request.requestBody?.toJson() - } - } - } - } - - override fun storeIncomingGossipingRequests(requests: List) { - doRealmTransactionAsync(realmConfiguration) { realm -> - requests.forEach { request -> - // After a clear cache, we might have a - realm.createObject(IncomingGossipingRequestEntity::class.java).let { - it.otherDeviceId = request.deviceId - it.otherUserId = request.userId - it.requestId = request.requestId ?: "" - it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis() - if (request is IncomingSecretShareRequest) { - it.type = GossipRequestType.SECRET - it.requestedInfoStr = request.secretName - } else if (request is IncomingRoomKeyRequest) { - it.type = GossipRequestType.KEY - it.requestedInfoStr = request.requestBody?.toJson() + override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestedIndex = newIndex } + } + } + + override fun updateOutgoingRoomKeyReply(roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + .findAll().firstOrNull { entity -> + entity.toOutgoingKeyRequest().let { + it.requestBody?.senderKey == senderKey && + it.requestBody?.algorithm == algorithm + } + }?.apply { + event.senderId?.let { addReply(it, fromDevice, event) } + } + } + } + + override fun deleteOutgoingRoomKeyRequest(requestId: String) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.deleteOnCascade() + } + } + + override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) + .findAll() + // I delete like this because I want to cascade delete replies? + .onEach { it.deleteOnCascade() } + } + } + + override fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.IncomingKeyRequest.name + val info = IncomingKeyRequestInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = fromUser, + deviceId = fromDevice, + requestId = requestId + ) + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let { + this.contentJson = it } } } } -// override fun getPendingIncomingSecretShareRequests(): List { -// return doRealmQueryAndCopyList(realmConfiguration) { -// it.where() -// .findAll() -// }.map { -// it.toIncomingSecretShareRequest() -// } -// } + override fun saveWithheldAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.OutgoingKeyWithheld.name + val info = WithheldInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + code = code, + userId = userId, + deviceId = deviceId + ) + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } + + override fun saveForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false) + } + + override fun saveIncomingForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true) + } + + private fun saveForwardKeyTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?, + incoming: Boolean + ) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name + val info = ForwardInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } /* ========================================================================================== * Cross Signing @@ -1504,45 +1468,45 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingSecretKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(inStates: Set): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .where(OutgoingKeyRequestEntity::class.java) + .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) } val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) + it.toOutgoingKeyRequest() } - val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + val trail = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) return trail } @@ -1707,29 +1671,23 @@ internal class RealmCryptoStore @Inject constructor( * So we need to tidy up a bit */ override fun tidyUpDataBase() { - val prevWeekTs = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1_000 + val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 doRealmTransaction(realmConfiguration) { realm -> - // Only keep one week history - realm.where() - .lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs) + // Clean the old ones? + realm.where() + .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") } + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } .deleteAllFromRealm() - // Clean the cancelled ones? - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") } - .deleteAllFromRealm() + // Only keep one month history - // Only keep one week history - realm.where() - .lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs) + val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L + realm.where() + .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") } + .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } .deleteAllFromRealm() // Can we do something for WithHeldSessionEntity? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index cac6499486..d621450ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -33,10 +33,14 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration { +internal class RealmCryptoStoreMigration @Inject constructor( + private val clock: Clock, +) : RealmMigration { /** * Forces all RealmCryptoStoreMigration instances to be equal * Avoids Realm throwing when multiple instances of the migration are set @@ -47,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - val schemaVersion = 15L + val schemaVersion = 16L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -59,7 +63,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration if (oldVersion < 5) MigrateCryptoTo005(realm).perform() if (oldVersion < 6) MigrateCryptoTo006(realm).perform() if (oldVersion < 7) MigrateCryptoTo007(realm).perform() - if (oldVersion < 8) MigrateCryptoTo008(realm).perform() + if (oldVersion < 8) MigrateCryptoTo008(realm, clock).perform() if (oldVersion < 9) MigrateCryptoTo009(realm).perform() if (oldVersion < 10) MigrateCryptoTo010(realm).perform() if (oldVersion < 11) MigrateCryptoTo011(realm).perform() @@ -67,5 +71,6 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration if (oldVersion < 13) MigrateCryptoTo013(realm).perform() if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform() + if (oldVersion < 16) MigrateCryptoTo016(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt index a2f2f8e97a..b43030e343 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -17,19 +17,19 @@ package org.matrix.android.sdk.internal.crypto.store.db import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -38,7 +38,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti /** * Realm module for Crypto store classes */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ CryptoMetadataEntity::class, CryptoRoomEntity::class, @@ -50,12 +51,13 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti KeyInfoEntity::class, CrossSigningInfoEntity::class, TrustLevelEntity::class, - GossipingEventEntity::class, - IncomingGossipingRequestEntity::class, - OutgoingGossipingRequestEntity::class, + AuditTrailEntity::class, + OutgoingKeyRequestEntity::class, + KeyRequestReplyEntity::class, MyDeviceLastSeenInfoEntity::class, WithHeldSessionEntity::class, SharedSessionEntity::class, OutboundGroupSessionInfoEntity::class - ]) + ] +) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt index 5e4b9b96da..8b7bf9c26b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt @@ -27,11 +27,13 @@ import javax.inject.Inject internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) { - private val signaturesAdapter = moshi.adapter>>(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) + private val signaturesAdapter = moshi.adapter>>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) fun update(keyInfo: KeyInfoEntity, cryptoCrossSigningKey: CryptoCrossSigningKey) { // update signatures? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt index 52d0124c2b..e5bdd2aa9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt @@ -72,16 +72,20 @@ internal class MigrateCryptoTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() - val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( - List::class.java, - String::class.java, - Any::class.java - )) - val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) + val listMigrationAdapter = moshi.adapter>( + Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + ) + ) + val mapMigrationAdapter = moshi.adapter( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) realm.schema.get("DeviceInfoEntity") ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt index e1d7598767..8ec2932a8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto.store.db.migration import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { @@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) realm.schema.remove("IncomingRoomKeyRequestEntity") // Not need to migrate existing request, just start fresh? - realm.schema.create("GossipingEventEntity") - .addField(GossipingEventEntityFields.TYPE, String::class.java) - .addIndex(GossipingEventEntityFields.TYPE) - .addField(GossipingEventEntityFields.CONTENT, String::class.java) - .addField(GossipingEventEntityFields.SENDER, String::class.java) - .addIndex(GossipingEventEntityFields.SENDER) - .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) - .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) - .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) - .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) - .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + .addField("type", String::class.java) + .addIndex("type") + .addField("content", String::class.java) + .addField("sender", String::class.java) + .addIndex("sender") + .addField("decryptionResultJson", String::class.java) + .addField("decryptionErrorCode", String::class.java) + .addField("ageLocalTs", Long::class.java) + .setNullable("ageLocalTs", true) + .addField("sendStateStr", String::class.java) realm.schema.create("IncomingGossipingRequestEntity") - .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) - .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) - .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) - .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("otherUserId", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("otherDeviceId", String::class.java) + .addField("requestStateStr", String::class.java) + .addField("localCreationTimestamp", Long::class.java) + .setNullable("localCreationTimestamp", true) realm.schema.create("OutgoingGossipingRequestEntity") - .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) - .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) - .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("recipientsData", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("requestStateStr", String::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt index 785e6a04f4..ad195e6e55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt @@ -21,8 +21,12 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator +import org.matrix.android.sdk.internal.util.time.Clock -internal class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) { +internal class MigrateCryptoTo008( + realm: DynamicRealm, + private val clock: Clock, +) : RealmMigrator(realm, 8) { override fun doMigrate(realm: DynamicRealm) { realm.schema.create("MyDeviceLastSeenInfoEntity") @@ -33,7 +37,7 @@ internal class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java) .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true) - val now = System.currentTimeMillis() + val now = clock.epochMillis() realm.schema.get("DeviceInfoEntity") ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt new file mode 100644 index 0000000000..5a14ebf85a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.remove("OutgoingGossipingRequestEntity") + realm.schema.remove("IncomingGossipingRequestEntity") + realm.schema.remove("GossipingEventEntity") + + // No need to migrate existing request, just start fresh + + val replySchema = realm.schema.create("KeyRequestReplyEntity") + .addField(KeyRequestReplyEntityFields.SENDER_ID, String::class.java) + .addField(KeyRequestReplyEntityFields.FROM_DEVICE, String::class.java) + .addField(KeyRequestReplyEntityFields.EVENT_JSON, String::class.java) + + realm.schema.create("OutgoingKeyRequestEntity") + .addField(OutgoingKeyRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_ID) + .addField(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID) + .addRealmListField(OutgoingKeyRequestEntityFields.REPLIES.`$`, replySchema) + .addField(OutgoingKeyRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingKeyRequestEntityFields.ROOM_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.ROOM_ID) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INDEX, Integer::class.java) + .addField(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, Long::class.java) + .setNullable(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, true) + + realm.schema.create("AuditTrailEntity") + .addField(AuditTrailEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(AuditTrailEntityFields.AGE_LOCAL_TS, true) + .addField(AuditTrailEntityFields.CONTENT_JSON, String::class.java) + .addField(AuditTrailEntityFields.TYPE, String::class.java) + .addIndex(AuditTrailEntityFields.TYPE) + + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, Boolean::class.java) + ?.transform { + // set the default value to true + it.setBoolean(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, true) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt similarity index 60% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt index d9a6f4fcba..2e0e9c8c8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,15 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.model +package org.matrix.android.sdk.internal.crypto.store.db.model -enum class GossipingRequestState { - NONE, - PENDING, - REJECTED, - ACCEPTING, - ACCEPTED, - FAILED_TO_ACCEPTED, +import io.realm.RealmObject +import io.realm.annotations.Index - // USER_REJECTED, - UNABLE_TO_PROCESS, - CANCELLED_BY_REQUESTER, - RE_REQUESTED +internal open class AuditTrailEntity( + var ageLocalTs: Long? = null, + @Index var type: String? = null, + var contentJson: String? = null +) : RealmObject() { + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt new file mode 100644 index 0000000000..80ae4a8d0d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.UnknownInfo +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal object AuditTrailMapper { + + fun map(entity: AuditTrailEntity): AuditTrail? { + val contentJson = entity.contentJson ?: return null + return when (entity.type) { + TrailType.OutgoingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyForward, + info = info + ) + } + TrailType.OutgoingKeyWithheld.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyWithheld, + info = info + ) + } + TrailType.IncomingKeyRequest.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyRequest, + info = info + ) + } + TrailType.IncomingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyForward, + info = info + ) + } + else -> { + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.Unknown, + info = UnknownInfo + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt index c71d5e73a2..ca41930f80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt @@ -27,21 +27,27 @@ import timber.log.Timber internal object CryptoMapper { private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() - private val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( - List::class.java, - String::class.java, - Any::class.java - )) - private val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) - private val mapOfStringMigrationAdapter = moshi.adapter>>(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) + private val listMigrationAdapter = moshi.adapter>( + Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + ) + ) + private val mapMigrationAdapter = moshi.adapter( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) + private val mapOfStringMigrationAdapter = moshi.adapter>>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity { return DeviceInfoEntity(primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId)) @@ -91,11 +97,13 @@ internal object CryptoMapper { }, keys = deviceInfoEntity.keysMapJson?.let { try { - moshi.adapter>(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )).fromJson(it) + moshi.adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ).fromJson(it) } catch (failure: Throwable) { Timber.e(failure) null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 35ae86db8b..63ed0e537e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity( var deviceSyncToken: String? = null, // Settings for blacklisting unverified devices. var globalBlacklistUnverifiedDevices: Boolean = false, + // setting to enable or disable key gossiping + var globalEnableKeyGossiping: Boolean = true, // The keys backup version currently used. Null means no backup. var backupVersion: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt deleted file mode 100644 index a024e092b7..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonDataException -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -import timber.log.Timber - -/** - * Keep track of gossiping event received in toDevice messages - * (room key request, or sss secret sharing, as well as cancellations) - * - */ -internal open class GossipingEventEntity(@Index var type: String? = "", - var content: String? = null, - @Index var sender: String? = null, - var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null, - var ageLocalTs: Long? = null) : RealmObject() { - - private var sendStateStr: String = SendState.UNKNOWN.name - - var sendState: SendState - get() { - return SendState.valueOf(sendStateStr) - } - set(value) { - sendStateStr = value.name - } - - companion object - - fun setDecryptionResult(result: MXEventDecryptionResult) { - val decryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) - decryptionResultJson = adapter.toJson(decryptionResult) - decryptionErrorCode = null - } - - fun toModel(): Event { - return Event( - type = this.type ?: "", - content = ContentMapper.map(this.content), - senderId = this.sender - ).also { - it.ageLocalTs = this.ageLocalTs - it.sendState = this.sendState - this.decryptionResultJson?.let { json -> - try { - it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) - } catch (t: JsonDataException) { - Timber.e(t, "Failed to parse decryption result") - } - } - // TODO get the full crypto error object - it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> - MXCryptoError.ErrorType.valueOf(errorCode) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt deleted file mode 100644 index f05c8853c8..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon - -internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", - @Index var typeStr: String? = null, - var otherUserId: String? = null, - var requestedInfoStr: String? = null, - var otherDeviceId: String? = null, - var localCreationTimestamp: Long? = null -) : RealmObject() { - - fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { - requestedInfoStr - } else null - - fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { - RoomKeyRequestBody.fromJson(requestedInfoStr) - } else null - - var type: GossipRequestType - get() { - return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = GossipingRequestState.NONE.name - - var requestState: GossipingRequestState - get() { - return tryOrNull { GossipingRequestState.valueOf(requestStateStr) } - ?: GossipingRequestState.NONE - } - set(value) { - requestStateStr = value.name - } - - companion object - - fun toIncomingGossipingRequest(): IncomingShareRequestCommon { - return when (type) { - GossipRequestType.KEY -> { - IncomingRoomKeyRequest( - requestBody = getRequestedKeyInfo(), - deviceId = otherDeviceId, - userId = otherUserId, - requestId = requestId, - state = requestState, - localCreationTimestamp = localCreationTimestamp - ) - } - GossipRequestType.SECRET -> { - IncomingSecretShareRequest( - secretName = getRequestedSecretName(), - deviceId = otherDeviceId, - userId = otherUserId, - requestId = requestId, - localCreationTimestamp = localCreationTimestamp - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt new file mode 100644 index 0000000000..0c7cf79e78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class KeyRequestReplyEntity( + var senderId: String? = null, + var fromDevice: String? = null, + var eventJson: String? = null +) : RealmObject() { + companion object + + fun getEvent(): Event? { + return eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt deleted file mode 100644 index 0e1278967e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest -import org.matrix.android.sdk.internal.di.MoshiProvider - -internal open class OutgoingGossipingRequestEntity( - @Index var requestId: String? = null, - var recipientsData: String? = null, - var requestedInfoStr: String? = null, - @Index var typeStr: String? = null -) : RealmObject() { - - fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { - requestedInfoStr - } else null - - fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { - RoomKeyRequestBody.fromJson(requestedInfoStr) - } else null - - var type: GossipRequestType - get() { - return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name - - var requestState: OutgoingGossipingRequestState - get() { - return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) } - ?: OutgoingGossipingRequestState.UNSENT - } - set(value) { - requestStateStr = value.name - } - - companion object { - - private val recipientsDataMapper: JsonAdapter>> = - MoshiProvider - .providesMoshi() - .adapter>>( - Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) - ) - } - - fun toOutgoingGossipingRequest(): OutgoingGossipingRequest { - return when (type) { - GossipRequestType.KEY -> { - OutgoingRoomKeyRequest( - requestBody = getRequestedKeyInfo(), - recipients = getRecipients().orEmpty(), - requestId = requestId ?: "", - state = requestState - ) - } - GossipRequestType.SECRET -> { - OutgoingSecretRequest( - secretName = getRequestedSecretName(), - recipients = getRecipients().orEmpty(), - requestId = requestId ?: "", - state = requestState - ) - } - } - } - - private fun getRecipients(): Map>? { - return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } - } - - fun setRecipients(recipients: Map>) { - this.recipientsData = recipientsDataMapper.toJson(recipients) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt new file mode 100644 index 0000000000..7a8ba18809 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.Index +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.RequestReply +import org.matrix.android.sdk.api.session.crypto.RequestResult +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class OutgoingKeyRequestEntity( + @Index var requestId: String? = null, + var requestedIndex: Int? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + var creationTimeStamp: Long? = null, + // de-normalization for better query (if not have to query all and parse json) + @Index var roomId: String? = null, + @Index var megolmSessionId: String? = null, + + var replies: RealmList = RealmList() +) : RealmObject() { + + @Index private var requestStateStr: String = OutgoingRoomKeyRequestState.UNSENT.name + + companion object { + + private val recipientsDataMapper: JsonAdapter>> = + MoshiProvider + .providesMoshi() + .adapter( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr) + + fun setRequestBody(body: RoomKeyRequestBody) { + requestedInfoStr = body.toJson() + roomId = body.roomId + megolmSessionId = body.sessionId + } + + var requestState: OutgoingRoomKeyRequestState + get() { + return tryOrNull { OutgoingRoomKeyRequestState.valueOf(requestStateStr) } + ?: OutgoingRoomKeyRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + private fun getRecipients(): Map>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } + + fun addReply(userId: String, fromDevice: String?, event: Event) { + val newReply = KeyRequestReplyEntity( + senderId = userId, + fromDevice = fromDevice, + eventJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(event) + ) + replies.add(newReply) + } + + fun toOutgoingKeyRequest(): OutgoingKeyRequest { + return OutgoingKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + fromIndex = requestedIndex ?: 0, + state = requestState, + results = replies.mapNotNull { entity -> + val userId = entity.senderId ?: return@mapNotNull null + val result = entity.eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + }?.let { event -> + eventToResult(event) + } ?: return@mapNotNull null + RequestReply( + userId = userId, + fromDevice = entity.fromDevice, + result = result + ) + } + ) + } + + private fun eventToResult(event: Event): RequestResult? { + return when (event.getClearType()) { + EventType.ROOM_KEY_WITHHELD -> { + event.content.toModel()?.code?.let { + RequestResult.Failure(it) + } + } + EventType.FORWARDED_ROOM_KEY -> { + RequestResult.Success((event.content?.get("chain_index") as? Number)?.toInt() ?: 0) + } + else -> null + } + } +} + +internal fun OutgoingKeyRequestEntity.deleteOnCascade() { + replies.deleteAllFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 1e395796a9..394c618968 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import dagger.Lazy import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult @@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task { internal class DefaultEncryptEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, - private val cryptoService: CryptoService + private val cryptoService: Lazy ) : EncryptEventTask { override suspend fun execute(params: EncryptEventTask.Params): Event { // don't want to wait for any query @@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor( // try { // let it throws awaitCallback { - cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) }.let { result -> val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> @@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() ) } else { null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index bdfe818c62..f1b1663756 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor( params.event.roomId ?.takeIf { params.encrypt } ?.let { roomId -> - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + tryOrNull { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } } val event = handleEncryption(params) @@ -73,11 +76,13 @@ internal class DefaultSendEventTask @Inject constructor( @Throws private suspend fun handleEncryption(params: SendEventTask.Params): Event { if (params.encrypt && !params.event.isEncrypted()) { - return encryptEventTask.execute(EncryptEventTask.Params( - params.event.roomId ?: "", - params.event, - listOf("m.relates_to") - )) + return encryptEventTask.execute( + EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to") + ) + ) } return params.event } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index c4a6ba27d6..7c52c62d92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) val response = executeRequest(globalErrorReceiver) { roomAPI.send( - localId, + txId = localId, roomId = event.roomId ?: "", content = event.content, eventType = event.type ?: "" @@ -64,11 +64,13 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) { try { - return encryptEventTask.execute(EncryptEventTask.Params( - params.event.roomId ?: "", - params.event, - listOf("m.relates_to") - )) + return encryptEventTask.execute( + EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to") + ) + ) } catch (throwable: Throwable) { // We said it's ok to send verification request in clear } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 68f1cf62d5..a9d5cf1191 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( override val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -47,13 +47,14 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserID, null, - isIncoming = true), + isIncoming = true +), IncomingSasVerificationTransaction { override val uxState: IncomingSasVerificationTransaction.UxState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index e203f03b06..ddb8ed232b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId: String?, cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -44,13 +44,14 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserId, otherDeviceId, - isIncoming = false), + isIncoming = false +), OutgoingSasVerificationTransaction { override val uxState: OutgoingSasVerificationTransaction.UxState @@ -123,7 +124,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( // val requestMessage = KeyVerificationRequest( // fromDevice = session.sessionParams.deviceId ?: "", // methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), -// timestamp = System.currentTimeMillis().toInt(), +// timestamp = clock.epochMillis().toInt(), // transactionId = transactionId // ) // diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 28bf1d70f7..cd503d6ed8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel @@ -84,6 +84,7 @@ import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -94,8 +95,8 @@ internal class DefaultVerificationService @Inject constructor( @UserId private val userId: String, @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -104,7 +105,8 @@ internal class DefaultVerificationService @Inject constructor( private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, private val crossSigningService: CrossSigningService, private val cryptoCoroutineScope: CoroutineScope, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val clock: Clock, ) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -261,9 +263,11 @@ internal class DefaultVerificationService @Inject constructor( } override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + setDeviceVerificationAction.handle( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), userId, - deviceID) + deviceID + ) listeners.forEach { try { @@ -313,7 +317,7 @@ internal class DefaultVerificationService @Inject constructor( val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), isIncoming = true, otherUserId = senderId, // requestInfo.toUserId, roomId = null, @@ -352,7 +356,7 @@ internal class DefaultVerificationService @Inject constructor( val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), isIncoming = true, otherUserId = senderId, // requestInfo.toUserId, roomId = event.roomId, @@ -547,12 +551,13 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, - autoAccept).also { txConfigure(it) } + autoAccept + ).also { txConfigure(it) } addTransaction(tx) tx.onVerificationStart(startReq) return null @@ -644,9 +649,11 @@ internal class DefaultVerificationService @Inject constructor( if (existingRequest != null) { // Mark this request as cancelled - updatePendingRequest(existingRequest.copy( - cancelConclusion = safeValueOf(cancelReq.code) - )) + updatePendingRequest( + existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + ) + ) } existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) @@ -764,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor( return } + val roomId = event.roomId + if (roomId == null) { + Timber.e("## SAS Verification missing roomId for event") + // TODO cancel? + return + } + handleReadyReceived(event.senderId, readyReq) { - verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + verificationTransportRoomMessageFactory.createTransport(roomId, it) } } @@ -807,17 +821,15 @@ internal class DefaultVerificationService @Inject constructor( getExistingTransaction(userId, doneReq.transactionId) ?: getOldTransaction(userId, doneReq.transactionId) ?.let { vt -> - val otherDeviceId = vt.otherDeviceId + val otherDeviceId = vt.otherDeviceId ?: return@let if (!crossSigningService.canCrossSign()) { - outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) - outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) - outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) + cryptoCoroutineScope.launch { + secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) + } } - outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId - ?: "*"))) } } } @@ -911,22 +923,25 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = senderId, otherDeviceId = readyReq.fromDevice, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, deviceId = deviceId ?: "", - isIncoming = false) + isIncoming = false + ) tx.transport = transportCreator.invoke(tx) addTransaction(tx) } - updatePendingRequest(existingRequest.copy( - readyInfo = readyReq - )) + updatePendingRequest( + existingRequest.copy( + readyInfo = readyReq + ) + ) } private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { @@ -1110,12 +1125,13 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, - otherDeviceId) + otherDeviceId + ) tx.transport = verificationTransportToDeviceFactory.createTransport(tx) addTransaction(tx) @@ -1150,7 +1166,7 @@ internal class DefaultVerificationService @Inject constructor( val validLocalId = localId ?: LocalEcho.createLocalEchoId() val verificationRequest = PendingVerificationRequest( - ageLocalTs = System.currentTimeMillis(), + ageLocalTs = clock.epochMillis(), isIncoming = false, roomId = roomId, localId = validLocalId, @@ -1173,16 +1189,18 @@ internal class DefaultVerificationService @Inject constructor( } .distinct() + requestsForUser.add(verificationRequest) transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> // We need to update with the syncedID - updatePendingRequest(verificationRequest.copy( - transactionId = syncedId, - // localId stays different - requestInfo = info - )) + updatePendingRequest( + verificationRequest.copy( + transactionId = syncedId, + // localId stays different + requestInfo = info + ) + ) } - requestsForUser.add(verificationRequest) dispatchRequestAdded(verificationRequest) return verificationRequest @@ -1228,7 +1246,7 @@ internal class DefaultVerificationService @Inject constructor( val verificationRequest = PendingVerificationRequest( transactionId = localId, - ageLocalTs = System.currentTimeMillis(), + ageLocalTs = clock.epochMillis(), isIncoming = false, roomId = null, localId = localId, @@ -1254,10 +1272,12 @@ internal class DefaultVerificationService @Inject constructor( transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info -> // Nothing special to do in to device mode - updatePendingRequest(verificationRequest.copy( - // localId stays different - requestInfo = info - )) + updatePendingRequest( + verificationRequest.copy( + // localId stays different + requestInfo = info + ) + ) } requestsForUser.add(verificationRequest) @@ -1271,9 +1291,11 @@ internal class DefaultVerificationService @Inject constructor( .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) getExistingVerificationRequest(otherUserId, transactionId)?.let { - updatePendingRequest(it.copy( - cancelConclusion = CancelCode.User - )) + updatePendingRequest( + it.copy( + cancelConclusion = CancelCode.User + ) + ) } } @@ -1302,12 +1324,13 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, - otherDeviceId) + otherDeviceId + ) tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) addTransaction(tx) @@ -1333,7 +1356,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId, existingRequest.requestInfo?.fromDevice ?: "", existingRequest.requestInfo?.methods, - methods) { + methods + ) { verificationTransportRoomMessageFactory.createTransport(roomId, it) } if (methods.isNullOrEmpty()) { @@ -1343,7 +1367,8 @@ internal class DefaultVerificationService @Inject constructor( } // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) - transport.sendToOther(EventType.KEY_VERIFICATION_READY, + transport.sendToOther( + EventType.KEY_VERIFICATION_READY, readyMsg, VerificationTxState.None, CancelCode.User, @@ -1372,7 +1397,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId, existingRequest.requestInfo?.fromDevice ?: "", existingRequest.requestInfo?.methods, - methods) { + methods + ) { verificationTransportToDeviceFactory.createTransport(it) } if (methods.isNullOrEmpty()) { @@ -1440,13 +1466,14 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = otherUserId, otherDeviceId = otherDeviceId, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, deviceId = deviceId ?: "", - isIncoming = false) + isIncoming = false + ) tx.transport = transportCreator.invoke(tx) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt index 69d9388c5f..1837b22445 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import timber.log.Timber @@ -31,8 +31,8 @@ import timber.log.Timber internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction( } if (otherUserId == userId) { - incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!) // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it @@ -105,8 +105,10 @@ internal abstract class DefaultVerificationTransaction( private fun setDeviceVerified(userId: String, deviceId: String) { // TODO should not override cross sign status - setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + setDeviceVerificationAction.handle( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), userId, - deviceId) + deviceId + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 4bf01a2809..4e82a6f406 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.extensions.toUnsignedInt @@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction( open val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -52,13 +52,14 @@ internal abstract class SASDefaultVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, otherDeviceId, - isIncoming), + isIncoming +), SasVerificationTransaction { companion object { @@ -297,9 +298,11 @@ internal abstract class SASDefaultVerificationTransaction( return } - trust(otherMasterKeyIsVerified, + trust( + otherMasterKeyIsVerified, verifiedDevices, - eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false) + eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false + ) } override fun cancel() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt deleted file mode 100644 index 0a175ae3ca..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import android.content.Context -import androidx.work.Data -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker -import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -/** - * Possible previous worker: None - * Possible next worker : None - */ -internal class SendVerificationMessageWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val eventId: String, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask - @Inject lateinit var localEchoRepository: LocalEchoRepository - @Inject lateinit var cancelSendTracker: CancelSendTracker - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found") - val localEventId = localEvent.eventId ?: "" - val roomId = localEvent.roomId ?: "" - - if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) { - return Result.success() - .also { - cancelSendTracker.markCancelled(localEventId, roomId) - Timber.e("## SendEvent: Event sending has been cancelled $localEventId") - } - } - - return try { - val resultEventId = sendVerificationMessageTask.execute( - SendVerificationMessageTask.Params( - event = localEvent - ) - ) - - Result.success(Data.Builder().putString(localEventId, resultEventId).build()) - } catch (throwable: Throwable) { - if (throwable.shouldBeRetried()) { - Result.retry() - } else { - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index 52166761ab..303b6c5839 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -15,13 +15,9 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import io.realm.Realm -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent @@ -29,20 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( - private val eventDecryptor: EventDecryptor, private val verificationService: DefaultVerificationService, @UserId private val userId: String, - @DeviceId private val deviceId: String? -) : EventInsertLiveProcessor { + @DeviceId private val deviceId: String?, + private val clock: Clock, +) { private val transactionsHandledByOtherDevice = ArrayList() @@ -58,41 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor( EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { - if (insertType != EventInsertType.INCREMENTAL_SYNC) { - return false - } - return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) + fun shouldProcess(eventType: String): Boolean { + return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + suspend fun process(event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}") // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. - if (!VerificationService.isValidRequest(event.ageLocalTs - ?: event.originServerTs)) return Unit.also { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms") } - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = eventDecryptor.decryptEvent(event, "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) - } - } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") // Relates to is not encrypted @@ -101,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor( if (event.senderId == userId) { // If it's send from me, we need to keep track of Requests or Start // done from another device of mine - if (EventType.MESSAGE == event.getClearType()) { val msgType = event.getClearContent().toModel()?.msgType if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { @@ -136,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor( transactionsHandledByOtherDevice.remove(it) verificationService.onRoomRequestHandledByOtherDevice(event) } + } else if (EventType.ENCRYPTED == event.getClearType()) { + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) } Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index 49235c5744..e32828af23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -15,14 +15,8 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import androidx.lifecycle.Observer -import androidx.work.BackoffPolicy -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.Operation -import androidx.work.WorkInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest @@ -45,25 +39,28 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit +import java.util.concurrent.Executors internal class VerificationTransportRoomMessage( - private val workManagerProvider: WorkManagerProvider, - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, private val userId: String, private val userDeviceId: String?, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val tx: DefaultVerificationTransaction?, - private val coroutineScope: CoroutineScope + cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, ) : VerificationTransport { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: VerificationTxState, @@ -77,68 +74,22 @@ internal class VerificationTransportRoomMessage( content = verificationInfo.toEventContent()!! ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) - val enqueueInfo = enqueueSendWork(workerParams) - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - -// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { -// override fun onSuccess(result: Operation.State.SUCCESS?) { -// if (onDone != null) { -// onDone() -// } else { -// tx?.state = nextState -// } -// } -// -// override fun onFailure(t: Throwable) { -// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") -// tx?.cancel(onErrorReason) -// } -// }, listenerExecutor) - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.firstOrNull { it.id == enqueueInfo.second } - ?.let { wInfo -> - when (wInfo.state) { - WorkInfo.State.FAILED -> { - tx?.cancel(onErrorReason) - workLiveData.removeObserver(this) - } - WorkInfo.State.SUCCEEDED -> { - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") - tx?.cancel(onErrorReason) - } else { - if (onDone != null) { - onDone() - } else { - tx?.state = nextState - } - } - workLiveData.removeObserver(this) - } - else -> { - // nop - } - } - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } catch (failure: Throwable) { + tx?.cancel(onErrorReason) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun sendVerificationRequest(supportedMethods: List, @@ -155,7 +106,7 @@ internal class VerificationTransportRoomMessage( transactionId = "", fromDevice = userDeviceId ?: "", methods = supportedMethods, - timestamp = System.currentTimeMillis() + timestamp = clock.epochMillis() ) val info = MessageVerificationRequestContent( @@ -169,58 +120,24 @@ internal class VerificationTransportRoomMessage( val content = info.toContent() val event = createEventAndLocalEcho( - localId, - EventType.MESSAGE, - roomId, - content + localId = localId, + type = EventType.MESSAGE, + roomId = roomId, + content = content ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } - ?.firstOrNull { it.id == workRequest.id } - ?.let { wInfo -> - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - callback(null, null) - } else { - val eventId = wInfo.outputData.getString(localId) - if (eventId != null) { - callback(eventId, validInfo) - } else { - callback(null, null) - } - } - workLiveData.removeObserver(this) - } + verificationSenderScope.launch { + val params = SendVerificationMessageTask.Params(event) + sequencer.post { + try { + val eventId = sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + callback(eventId, validInfo) + } catch (failure: Throwable) { + callback(null, null) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { @@ -230,11 +147,17 @@ internal class VerificationTransportRoomMessage( roomId = roomId, content = MessageVerificationCancelContent.create(transactionId, code).toContent() ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) - enqueueSendWork(workerParams) + + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to cancel verification transaction") + } + } + } } override fun done(transactionId: String, @@ -250,45 +173,21 @@ internal class VerificationTransportRoomMessage( ) ).toContent() ) - val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - )) - val enqueueInfo = enqueueSendWork(workerParams) - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } - ?.firstOrNull { it.id == enqueueInfo.second } - ?.let { _ -> - onDone?.invoke() - workLiveData.removeObserver(this) - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to complete (done) verification") + // should we call onDone? + } finally { + onDone?.invoke() + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } - private fun enqueueSendWork(workerParams: Data): Pair { - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - return workManagerProvider.workManager - .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() to workRequest.id - } - - private fun uniqueQueueName() = "${roomId}_VerificationWork" - override fun createAccept(tid: String, keyAgreementProtocol: String, hash: String, @@ -361,7 +260,7 @@ internal class VerificationTransportRoomMessage( private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, - originServerTs = System.currentTimeMillis(), + originServerTs = clock.epochMillis(), senderId = userId, eventId = localId, type = type, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt index f89127273b..345948e608 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt @@ -16,34 +16,35 @@ package org.matrix.android.sdk.internal.crypto.verification +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportRoomMessageFactory @Inject constructor( - private val workManagerProvider: WorkManagerProvider, - @SessionId - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, @UserId private val userId: String, @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, - private val taskExecutor: TaskExecutor + private val cryptoCoroutineScope: CoroutineScope, + private val clock: Clock, ) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { - return VerificationTransportRoomMessage(workManagerProvider, - sessionId, - userId, - deviceId, - roomId, - localEchoEventFactory, - tx, - taskExecutor.executorScope) + return VerificationTransportRoomMessage( + sendVerificationMessageTask = sendVerificationMessageTask, + userId = userId, + userDeviceId = deviceId, + roomId = roomId, + localEchoEventFactory = localEchoEventFactory, + tx = tx, + cryptoCoroutineScope = cryptoCoroutineScope, + clock = clock, + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt index 40deda2745..bc24ef2966 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -35,13 +35,16 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber +// TODO var could be val internal class VerificationTransportToDevice( private var tx: DefaultVerificationTransaction?, private var sendToDeviceTask: SendToDeviceTask, private val myDeviceId: String?, - private var taskExecutor: TaskExecutor + private var taskExecutor: TaskExecutor, + private val clock: Clock, ) : VerificationTransport { override fun sendVerificationRequest(supportedMethods: List, @@ -56,7 +59,7 @@ internal class VerificationTransportToDevice( transactionId = localId, fromDevice = myDeviceId ?: "", methods = supportedMethods, - timestamp = System.currentTimeMillis() + timestamp = clock.epochMillis() ) val keyReq = KeyVerificationRequest( fromDevice = validKeyReq.fromDevice, @@ -201,7 +204,8 @@ internal class VerificationTransportToDevice( hash, commitment, messageAuthenticationCode, - shortAuthenticationStrings) + shortAuthenticationStrings + ) override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) @@ -221,7 +225,8 @@ internal class VerificationTransportToDevice( hashes, messageAuthenticationCodes, shortAuthenticationStrings, - null) + null + ) } override fun createStartForQrCode(fromDevice: String, @@ -235,7 +240,8 @@ internal class VerificationTransportToDevice( null, null, null, - sharedSecret) + sharedSecret + ) } override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt index e9a2c65ef7..312d911822 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt @@ -19,14 +19,17 @@ package org.matrix.android.sdk.internal.crypto.verification import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportToDeviceFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, @DeviceId val myDeviceId: String?, - private val taskExecutor: TaskExecutor) { + private val taskExecutor: TaskExecutor, + private val clock: Clock, +) { fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { - return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor) + return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor, clock) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 06f0b36798..b1ad4d87b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction( override val otherUserId: String, override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -48,13 +48,14 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, otherDeviceId, - isIncoming), + isIncoming +), QrCodeVerificationTransaction { override val qrCodeText: String? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt index 0ac57db9bc..34a9525194 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt @@ -56,7 +56,8 @@ internal sealed class QrCodeData( transactionId, userMasterCrossSigningPublicKey, otherUserMasterCrossSigningPublicKey, - sharedSecret) + sharedSecret + ) /** * self-verifying in which the current device does trust the master key @@ -77,7 +78,8 @@ internal sealed class QrCodeData( transactionId, userMasterCrossSigningPublicKey, otherDeviceKey, - sharedSecret) + sharedSecret + ) /** * self-verifying in which the current device does not yet trust the master key @@ -98,5 +100,6 @@ internal sealed class QrCodeData( transactionId, deviceKey, userMasterCrossSigningPublicKey, - sharedSecret) + sharedSecret + ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index 315d77d932..7d263f1937 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import kotlin.system.measureTimeMillis internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { asyncTransaction(monarchy.realmConfiguration, transaction) @@ -41,13 +42,13 @@ internal suspend fun awaitTransaction(config: RealmConfiguration, transactio bgRealm.beginTransaction() val result: T try { - val start = System.currentTimeMillis() - result = transaction(bgRealm) - if (isActive) { - bgRealm.commitTransaction() - val end = System.currentTimeMillis() - val time = end - start - Timber.v("Execute transaction in $time millis") + measureTimeMillis { + result = transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + } + }.also { + Timber.v("Execute transaction in $it millis") } } finally { if (bgRealm.isInTransaction) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 24ac310653..04a6e83ea1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -59,7 +60,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 27L + val schemaVersion = 28L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -91,5 +92,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 25) MigrateSessionTo025(realm).perform() if (oldVersion < 26) MigrateSessionTo026(realm).perform() if (oldVersion < 27) MigrateSessionTo027(realm).perform() + if (oldVersion < 28) MigrateSessionTo028(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 04cf5b78af..03b1948302 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -207,8 +207,10 @@ internal fun List.mapEventsWithEdition(realm: Realm, roomId: Stri ?.eventId ?.let { editedEventId -> TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent -> - it.root.threadDetails = it.root.threadDetails?.copy(lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() - ?: "(edited)") + it.root.threadDetails = it.root.threadDetails?.copy( + lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() + ?: "(edited)" + ) it } ?: it } ?: it @@ -341,7 +343,8 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: realm = realm, roomId = roomId, rootThreadEventId = eventId, - senderId = currentUserId) + senderId = currentUserId + ) val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst() if (isUserParticipating) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 89474a1bc5..d052a7dea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -137,7 +137,8 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( roomMemberContentsByUser: HashMap, roomEntity: RoomEntity, userId: String, - cryptoService: CryptoService? = null + cryptoService: CryptoService? = null, + currentTimeMillis: Long, ) { when (threadSummaryType) { ThreadSummaryUpdateType.REPLACE -> { @@ -153,14 +154,14 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") } - val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also { + val rootThreadEventEntity = createEventEntity(realm, roomId, rootThreadEvent, currentTimeMillis).also { try { decryptIfNeeded(cryptoService, it, roomId) } catch (e: InterruptedException) { Timber.i("Decryption got interrupted") } } - val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also { + val latestThreadEventEntity = createLatestEventEntity(realm, roomId, rootThreadEvent, roomMemberContentsByUser, currentTimeMillis)?.also { try { decryptIfNeeded(cryptoService, it, roomId) } catch (e: InterruptedException) { @@ -268,8 +269,8 @@ private fun HashMap.addSenderState(realm: Realm, roo /** * Create an EventEntity for the root thread event or get an existing one */ -private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { - val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } +private fun createEventEntity(realm: Realm, roomId: String, event: Event, currentTimeMillis: Long): EventEntity { + val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it } return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } @@ -278,15 +279,17 @@ private fun createEventEntity(roomId: String, event: Event, realm: Realm): Event * state */ private fun createLatestEventEntity( + realm: Realm, roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap, - realm: Realm): EventEntity? { + currentTimeMillis: Long, +): EventEntity? { return getLatestEvent(rootThreadEvent)?.let { it.senderId?.let { senderId -> roomMemberContentsByUser.addSenderState(realm, roomId, senderId) } - createEventEntity(roomId, it, realm) + createEventEntity(realm, roomId, it, currentTimeMillis) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 3083df062e..bc7d40bf6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -30,13 +30,14 @@ import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber +import kotlin.random.Random internal object EventMapper { fun map(event: Event, roomId: String): EventEntity { val eventEntity = EventEntity() // TODO change this as we shouldn't use event everywhere - eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" + eventEntity.eventId = event.eventId ?: "$$roomId-${Random.nextLong()}-${event.hashCode()}" eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent()) @@ -126,7 +127,10 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event { return EventMapper.map(this, castJsonNumbers) } -internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity { +internal fun Event.toEntity(roomId: String, + sendState: SendState, + ageLocalTs: Long?, + contentToInject: String? = null): EventEntity { return EventMapper.map(this, roomId).apply { this.sendState = sendState this.ageLocalTs = ageLocalTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo028.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo028.kt new file mode 100644 index 0000000000..1d0c638d7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo028.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary + */ +internal class MigrateSessionTo028(realm: DynamicRealm) : RealmMigrator(realm, 28) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.takeIf { !it.hasPrimaryKey() } + ?.addPrimaryKey(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 9a92b14510..be5a500956 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -24,7 +24,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit /** * Realm module for Session */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ ChunkEntity::class, EventEntity::class, @@ -71,5 +72,6 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit SpaceParentSummaryEntity::class, UserPresenceEntity::class, ThreadSummaryEntity::class - ]) + ] +) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index 2fad2d8e78..dbc6aac6b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -43,15 +43,17 @@ import org.matrix.android.sdk.internal.worker.MatrixWorkerFactory import org.matrix.olm.OlmManager import java.io.File -@Component(modules = [ - MatrixModule::class, - NetworkModule::class, - AuthModule::class, - RawModule::class, - SettingsModule::class, - SystemModule::class, - NoOpTestModule::class -]) +@Component( + modules = [ + MatrixModule::class, + NetworkModule::class, + AuthModule::class, + RawModule::class, + SettingsModule::class, + SystemModule::class, + NoOpTestModule::class + ] +) @MatrixScope internal interface MatrixComponent { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt index 9cab307c61..49713a1d7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -36,7 +36,8 @@ internal object MatrixModule { @Provides @MatrixScope fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { - return MatrixCoroutineDispatchers(io = Dispatchers.IO, + return MatrixCoroutineDispatchers( + io = Dispatchers.IO, computation = Dispatchers.Default, main = Dispatchers.Main, crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt index 10b0d4fb13..8f007f227c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt @@ -46,17 +46,18 @@ internal object MoshiProvider { .add(TlsVersionMoshiAdapter()) // Use addLast here so we can inject a SplitLazyRoomSyncJsonAdapter later to override the default parsing. .addLast(DefaultLazyRoomSyncEphemeralJsonAdapter()) - .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) - .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) - .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) - .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) - .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) - .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) - .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) - .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) - .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) - .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) - .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE) + .add( + RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) + .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) + .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) + .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) + .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) + .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) + .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) + .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) + .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) + .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE) ) .add(SerializeNulls.JSON_ADAPTER_FACTORY) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index fedd7d05f9..60760be29f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -84,9 +84,11 @@ internal class WorkManagerProvider @Inject constructor( if (workInfo?.state?.isFinished == true) { checkWorkerLiveState.removeObserver(this) if (workInfo.state == WorkInfo.State.FAILED) { - throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" + - "Makes sure to add it to a DelegatingWorkerFactory if you have your own factory or use it directly.\n" + - "You can grab the instance through the Matrix class.") + throw RuntimeException( + "MatrixWorkerFactory is not being set on your worker configuration.\n" + + "Makes sure to add it to a DelegatingWorkerFactory if you have your own factory or use it directly.\n" + + "You can grab the instance through the Matrix class." + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt index 3b4bd1b1a3..087f99ba7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt @@ -76,10 +76,12 @@ class WellKnown { if (apiUrl != null && apiUrl.startsWith("https://") && uiUrl!!.startsWith("https://")) { - managers.add(WellKnownManagerConfig( - apiUrl = apiUrl, - uiUrl = uiUrl - )) + managers.add( + WellKnownManagerConfig( + apiUrl = apiUrl, + uiUrl = uiUrl + ) + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 6efa347d3a..8b54978279 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -33,7 +33,7 @@ internal interface CheckNumberType { companion object { val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { @Nullable - override fun create(type: Type, annotations: Set?, moshi: Moshi): JsonAdapter<*>? { + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type !== Any::class.java) { return null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt index 0aaa4991cd..40d174ee2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt @@ -63,8 +63,10 @@ internal class RuntimeJsonAdapterFactory( } val fallbackAdapter = moshi.adapter(fallbackType) val objectJsonAdapter = moshi.adapter(Any::class.java) - return RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel, - objectJsonAdapter, fallbackAdapter).nullSafe() + return RuntimeJsonAdapter( + labelKey, labelToAdapter, typeToLabel, + objectJsonAdapter, fallbackAdapter + ).nullSafe() } @Suppress("UNCHECKED_CAST") @@ -77,8 +79,10 @@ internal class RuntimeJsonAdapterFactory( override fun fromJson(reader: JsonReader): Any? { val peekedToken = reader.peek() if (peekedToken != JsonReader.Token.BEGIN_OBJECT) { - throw JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken + - " at path " + reader.path) + throw JsonDataException( + "Expected BEGIN_OBJECT but was " + peekedToken + + " at path " + reader.path + ) } val jsonValue = reader.readJsonValue() val jsonObject = jsonValue as Map? @@ -91,13 +95,15 @@ internal class RuntimeJsonAdapterFactory( override fun toJson(writer: JsonWriter, value: Any?) { val type: Class<*> = value!!.javaClass val label = typeToLabel[type] - ?: throw IllegalArgumentException("Expected one of " + - typeToLabel.keys + - " but found " + - value + - ", a " + - value.javaClass + - ". Register this subtype.") + ?: throw IllegalArgumentException( + "Expected one of " + + typeToLabel.keys + + " but found " + + value + + ", a " + + value.javaClass + + ". Register this subtype." + ) val adapter = labelToAdapter[label]!! val jsonValue = adapter.toJsonValue(value) as Map? val valueWithLabel: MutableMap = LinkedHashMap(1 + jsonValue!!.size) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt index 7294ce4abf..c5ea2d48ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt @@ -35,8 +35,10 @@ internal fun RealmQuery.process(sortOrder: RoomSortOrder): Re arrayOf( RoomSummaryEntityFields.IS_FAVOURITE, RoomSummaryEntityFields.IS_LOW_PRIORITY, - RoomSummaryEntityFields.LAST_ACTIVITY_TIME), - arrayOf(Sort.DESCENDING, Sort.ASCENDING, Sort.DESCENDING)) + RoomSummaryEntityFields.LAST_ACTIVITY_TIME + ), + arrayOf(Sort.DESCENDING, Sort.ASCENDING, Sort.DESCENDING) + ) } RoomSortOrder.NONE -> { } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt index 770a49c904..c95e4316e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt @@ -23,9 +23,11 @@ import org.matrix.android.sdk.internal.database.model.RawCacheEntity /** * Realm module for global classes */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ RawCacheEntity::class, KnownServerUrlEntity::class - ]) + ] +) internal class GlobalRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 9b83cca558..78f1c84f3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.util.file.AtomicFileCreator +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.writeToFile import timber.log.Timber import java.io.File @@ -51,7 +52,8 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @UnauthenticatedWithCertificateWithProgress private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, ) : FileService { // Legacy folder, will be deleted @@ -182,7 +184,8 @@ internal class DefaultFileService @Inject constructor( MXEncryptedAttachments.decryptAttachment( inputStream, elementToDecrypt, - outputStream + outputStream, + clock ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 76e5d84e56..050480e6c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -21,12 +21,8 @@ import dagger.Component import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker import org.matrix.android.sdk.internal.crypto.CryptoModule -import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker -import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.federation.FederationModule import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker @@ -68,7 +64,8 @@ import org.matrix.android.sdk.internal.session.widgets.WidgetModule import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.system.SystemModule -@Component(dependencies = [MatrixComponent::class], +@Component( + dependencies = [MatrixComponent::class], modules = [ SessionModule::class, RoomModule::class, @@ -132,14 +129,6 @@ internal interface SessionComponent { fun inject(worker: AddPusherWorker) - fun inject(worker: SendVerificationMessageWorker) - - fun inject(worker: SendGossipRequestWorker) - - fun inject(worker: CancelGossipRequestWorker) - - fun inject(worker: SendGossipWorker) - fun inject(worker: UpdateTrustWorker) @Component.Factory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 0aae9f3105..7ceb89e892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask -import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory @@ -318,10 +317,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor - @Binds - @IntoSet - abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor - @Binds @IntoSet abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 59058bf976..c4f711a9e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerConten import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -41,9 +42,12 @@ private val loggerTag = LoggerTag("CallSignalingHandler", LoggerTag.VOIP) private const val MAX_AGE_TO_RING = 40_000 @SessionScope -internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler, - private val mxCallFactory: MxCallFactory, - @UserId private val userId: String) { +internal class CallSignalingHandler @Inject constructor( + private val activeCallHandler: ActiveCallHandler, + private val mxCallFactory: MxCallFactory, + @UserId private val userId: String, + private val clock: Clock, +) { private val invitedCallIds = mutableSetOf() private val callListeners = mutableSetOf() @@ -184,7 +188,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa if (event.roomId == null || event.senderId == null) { return } - val now = System.currentTimeMillis() + val now = clock.epochMillis() val age = now - (event.ageLocalTs ?: now) if (age > MAX_AGE_TO_RING) { Timber.tag(loggerTag.value).w("Call invite is too old to ring.") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt index d53ddb7371..1313fcaa62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt @@ -22,16 +22,16 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal abstract class GetTurnServerTask : Task { - object Params -} +internal abstract class GetTurnServerTask : Task -internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: VoipApi, - private val globalErrorReceiver: GlobalErrorReceiver) : GetTurnServerTask() { +internal class DefaultGetTurnServerTask @Inject constructor( + private val voipApi: VoipApi, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetTurnServerTask() { - override suspend fun execute(params: Params): TurnServerResponse { + override suspend fun execute(params: Unit): TurnServerResponse { return executeRequest(globalErrorReceiver) { - voipAPI.getTurnServer() + voipApi.getTurnServer() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index 547be2253f..9ec892b65d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MxCallFactory @Inject constructor( @@ -36,7 +37,8 @@ internal class MxCallFactory @Inject constructor( private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, private val getProfileInfoTask: GetProfileInfoTask, - @UserId private val userId: String + @UserId private val userId: String, + private val clock: Clock, ) { fun createIncomingCall(roomId: String, opponentUserId: String, content: CallInviteContent): MxCall? { @@ -51,7 +53,8 @@ internal class MxCallFactory @Inject constructor( localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, - getProfileInfoTask = getProfileInfoTask + getProfileInfoTask = getProfileInfoTask, + clock = clock, ).apply { updateOpponentData(opponentUserId, content, content.capabilities) } @@ -68,7 +71,8 @@ internal class MxCallFactory @Inject constructor( localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, - getProfileInfoTask = getProfileInfoTask + getProfileInfoTask = getProfileInfoTask, + clock = clock, ).apply { // Setup with this userId, might be updated when processing the Answer event this.opponentUserId = opponentUserId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt index 8e2ac5e17e..126a581fa4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt @@ -39,7 +39,7 @@ internal class TurnServerDataSource @Inject constructor(private val turnServerTa } suspend fun getTurnServer(): TurnServerResponse { - return cachedTurnServerResponse.data ?: turnServerTask.execute(GetTurnServerTask.Params).also { + return cachedTurnServerResponse.data ?: turnServerTask.execute(Unit).also { cachedTurnServerResponse.data = it } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index a89713870a..796e83311f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.math.BigDecimal @@ -61,7 +62,8 @@ internal class MxCallImpl( private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, - private val getProfileInfoTask: GetProfileInfoTask + private val getProfileInfoTask: GetProfileInfoTask, + private val clock: Clock, ) : MxCall { override var opponentPartyId: Optional? = null @@ -250,7 +252,7 @@ internal class MxCallImpl( private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, - originServerTs = System.currentTimeMillis(), + originServerTs = clock.epochMillis(), senderId = userId, eventId = localId, type = type, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 75606f2e7a..d3de807b23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.util.TemporaryFileCreator +import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams @@ -87,6 +88,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var thumbnailExtractor: ThumbnailExtractor @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var temporaryFileCreator: TemporaryFileCreator + @Inject lateinit var clock: Clock override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -243,7 +245,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter .also { filesToDelete.add(it) } uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), encryptedFile) { read, total -> + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), encryptedFile, clock) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -287,12 +289,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val uploadThumbnailResult = dealWithThumbnail(params) - handleSuccess(params, + handleSuccess( + params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadThumbnailResult?.uploadedThumbnailUrl, uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo, - newAttachmentAttributes) + newAttachmentAttributes + ) } catch (t: Throwable) { Timber.e(t, "## ERROR ${t.localizedMessage}") handleFailure(params, t) @@ -329,7 +333,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter if (params.isEncrypted) { Timber.v("Encrypt thumbnail") notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream()) + val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), clock) val contentUploadResponse = fileUploader.uploadByteArray( byteArray = encryptionResult.encryptedByteArray, filename = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt index da7e2d102e..5aaf058757 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPub import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -46,7 +47,8 @@ internal class DefaultContentScannerService @Inject constructor( private val getServerPublicKeyTask: GetServerPublicKeyTask, private val scanEncryptedTask: ScanEncryptedTask, private val scanMediaTask: ScanMediaTask, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val clock: Clock, ) : ContentScannerService { // Cache public key in memory @@ -71,11 +73,13 @@ internal class DefaultContentScannerService @Inject constructor( override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo { val result = if (fileInfo != null) { - scanEncryptedTask.execute(ScanEncryptedTask.Params( - mxcUrl = mxcUrl, - publicServerKey = getServerPublicKey(false), - encryptedInfo = fileInfo - )) + scanEncryptedTask.execute( + ScanEncryptedTask.Params( + mxcUrl = mxcUrl, + publicServerKey = getServerPublicKey(false), + encryptedInfo = fileInfo + ) + ) } else { scanMediaTask.execute(ScanMediaTask.Params(mxcUrl)) } @@ -83,7 +87,7 @@ internal class DefaultContentScannerService @Inject constructor( return ScanStatusInfo( state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED, humanReadableMessage = result.info, - scanDateTimestamp = System.currentTimeMillis() + scanDateTimestamp = clock.epochMillis() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt index b47be235c6..e4b64a1a0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt @@ -31,11 +31,14 @@ internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: .findFirst() } -internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity { +internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, + attachmentUrl: String, + contentScannerUrl: String?, + currentTimeMillis: Long): ContentScanResultEntity { return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl) ?: realm.createObject().also { it.mediaUrl = attachmentUrl - it.scanDateTimestamp = System.currentTimeMillis() + it.scanDateTimestamp = currentTimeMillis it.scannerUrl = contentScannerUrl } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt index bb53140ad9..85c1947a80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt @@ -21,9 +21,11 @@ import io.realm.annotations.RealmModule /** * Realm module for content scanner classes */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ ContentScannerInfoEntity::class, ContentScanResultEntity::class - ]) + ] +) internal class ContentScannerRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt index 947a66c8b9..27729d38c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt @@ -32,12 +32,14 @@ import org.matrix.android.sdk.internal.di.ContentScannerDatabase import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore import org.matrix.android.sdk.internal.util.isValidUrl +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @SessionScope internal class RealmContentScannerStore @Inject constructor( @ContentScannerDatabase - private val realmConfiguration: RealmConfiguration + private val realmConfiguration: RealmConfiguration, + private val clock: Clock, ) : ContentScannerStore { private val monarchy = Monarchy.Builder() @@ -82,15 +84,15 @@ internal class RealmContentScannerStore @Inject constructor( override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) { monarchy.runTransactionSync { - ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state + ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl, clock.epochMillis()).scanResult = state } } override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) { monarchy.runTransactionSync { - ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply { + ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl, clock.epochMillis()).apply { scanResult = state - scanDateTimestamp = System.currentTimeMillis() + scanDateTimestamp = clock.epochMillis() humanReadableMessage = humanReadable } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 44e13d971a..e9097e4d03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -94,10 +94,12 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( }.getOrNull() val wellknownResult = runCatching { - getWellknownTask.execute(GetWellknownTask.Params( - domain = userId.getDomain(), - homeServerConnectionConfig = homeServerConnectionConfig - )) + getWellknownTask.execute( + GetWellknownTask.Params( + domain = userId.getDomain(), + homeServerConnectionConfig = homeServerConnectionConfig + ) + ) }.getOrNull() insertInDb(capabilities, mediaConfig, versions, wellknownResult) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index 4285f38893..74838afc65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -218,9 +218,11 @@ internal class DefaultIdentityService @Inject constructor( listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } } } - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( - identityContent = IdentityServerContent(baseUrl = url) - )) + updateUserAccountDataTask.execute( + UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityServerContent(baseUrl = url) + ) + ) } override fun getUserConsent(): Boolean { @@ -297,11 +299,13 @@ internal class DefaultIdentityService @Inject constructor( } override suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String): SignInvitationResult { - return sign3pidInvitationTask.execute(Sign3pidInvitationTask.Params( - url = identiyServer, - token = token, - privateKey = secret - )) + return sign3pidInvitationTask.execute( + Sign3pidInvitationTask.Params( + url = identiyServer, + token = token, + privateKey = secret + ) + ) } override fun addListener(listener: IdentityServiceListener) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index f6ef370f8d..f642ed4cf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -83,11 +83,13 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return try { LookUpData(hashedAddresses, executeRequest(null) { - identityAPI.lookup(IdentityLookUpParams( - hashedAddresses, - IdentityHashDetailResponse.ALGORITHM_SHA256, - hashDetailResponse.pepper - )) + identityAPI.lookup( + IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + ) + ) }) } catch (failure: Throwable) { // Catch invalid hash pepper and retry @@ -117,8 +119,10 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return withOlmUtility { olmUtility -> threePids.map { threePid -> base64ToBase64Url( - olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) + - " " + threePid.toMedium() + " " + pepper) + olmUtility.sha256( + threePid.value.lowercase(Locale.ROOT) + + " " + threePid.toMedium() + " " + pepper + ) ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt index 9c89048176..fe12309650 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -57,18 +57,22 @@ internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( val tokenResponse = executeRequest(null) { when (params.threePid) { - is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( - clientSecret = clientSecret, - sendAttempt = sendAttempt, - email = params.threePid.email - )) + is ThreePid.Email -> identityAPI.requestTokenToBindEmail( + IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + ) + ) is ThreePid.Msisdn -> { - identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( - clientSecret = clientSecret, - sendAttempt = sendAttempt, - phoneNumber = params.threePid.msisdn, - countryCode = params.threePid.getCountryCode() - )) + identityAPI.requestTokenToBindMsisdn( + IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + ) + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt index f884e2816d..fcf8ce6317 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -50,7 +50,8 @@ internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( clientSecret = identityPendingBinding.clientSecret, sid = identityPendingBinding.sid, token = params.token - )) + ) + ) } if (!tokenResponse.isSuccess()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt index 1f2cfad33c..5e9068ecf7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt @@ -21,9 +21,11 @@ import io.realm.annotations.RealmModule /** * Realm module for identity server classes */ -@RealmModule(library = true, +@RealmModule( + library = true, classes = [ IdentityDataEntity::class, IdentityPendingBindingEntity::class - ]) + ] +) internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt index 87e51181e6..f630c2c225 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt @@ -50,7 +50,8 @@ internal class DefaultBindThreePidsTask @Inject constructor(private val profileA identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, identityServerAccessToken = identityServerAccessToken, sid = identityPendingBinding.sid - )) + ) + ) } // Binding is over, cleanup the store diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 6f99577ac2..5e64a6af0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -135,21 +135,25 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto override suspend fun finalizeAddingThreePid(threePid: ThreePid, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { finalizeAddingThreePidTask - .execute(FinalizeAddingThreePidTask.Params( - threePid = threePid, - userInteractiveAuthInterceptor = userInteractiveAuthInterceptor, - userWantsToCancel = false - )) + .execute( + FinalizeAddingThreePidTask.Params( + threePid = threePid, + userInteractiveAuthInterceptor = userInteractiveAuthInterceptor, + userWantsToCancel = false + ) + ) refreshThreePids() } override suspend fun cancelAddingThreePid(threePid: ThreePid) { finalizeAddingThreePidTask - .execute(FinalizeAddingThreePidTask.Params( - threePid = threePid, - userInteractiveAuthInterceptor = null, - userWantsToCancel = true - )) + .execute( + FinalizeAddingThreePidTask.Params( + threePid = threePid, + userInteractiveAuthInterceptor = null, + userWantsToCancel = true + ) + ) refreshThreePids() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt index df8a1c97ff..acbecc9fbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt @@ -45,7 +45,8 @@ internal class DefaultUnbindThreePidsTask @Inject constructor(private val profil identityServerUrlWithoutProtocol, params.threePid.toMedium(), params.threePid.value - )) + ) + ) }.isSuccess() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index e87c27e601..13b990a9ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -85,17 +85,19 @@ internal class DefaultPushersService @Inject constructor( deviceDisplayName: String, append: Boolean) { addPusherTask.execute( - AddPusherTask.Params(JsonPusher( - pushKey = email, - kind = Pusher.KIND_EMAIL, - appId = Pusher.APP_ID_EMAIL, - profileTag = "", - lang = lang, - appDisplayName = appDisplayName, - deviceDisplayName = deviceDisplayName, - data = JsonPusherData(brand = emailBranding), - append = append - )) + AddPusherTask.Params( + JsonPusher( + pushKey = email, + kind = Pusher.KIND_EMAIL, + appId = Pusher.APP_ID_EMAIL, + profileTag = "", + lang = lang, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + data = JsonPusherData(brand = emailBranding), + append = append + ) + ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt index 91d092a2b4..60c1194708 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt @@ -67,8 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor( }.filter { it.senderId != userId } - Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + - " to check for push rules with ${params.rules.size} rules") + Timber.v( + "[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + + " to check for push rules with ${params.rules.size} rules" + ) val matchedEvents = allEvents.mapNotNull { event -> pushRuleFinder.fulfilledBingRule(event, params.rules)?.let { Timber.v("[PushRules] Rule $it match for event ${event.eventId}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index a8158d9bef..7e0b44a314 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -69,6 +69,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -77,7 +78,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val stateEventDataSource: StateEventDataSource, @SessionId private val sessionId: String, private val sessionManager: SessionManager, - private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor + private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, + private val clock: Clock, ) : EventInsertLiveProcessor { private val allowedTypes = listOf( @@ -338,7 +340,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.editions.firstOrNull { it.eventId == txId }?.let { it.eventId = event.eventId - it.timestamp = event.originServerTs ?: System.currentTimeMillis() + it.timestamp = event.originServerTs ?: clock.epochMillis() it.isLocalEcho = false } } else { @@ -349,10 +351,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( eventId = event.eventId, content = ContentMapper.map(newContent), timestamp = if (isLocalEcho) { - System.currentTimeMillis() + clock.epochMillis() } else { // Do not take local echo originServerTs here, could mess up ordering (keep old ts) - event.originServerTs ?: System.currentTimeMillis() + event.originServerTs ?: clock.epochMillis() }, isLocalEcho = isLocalEcho ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 3b2e9d3d22..faf68d538f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -116,7 +116,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( fileUploader.uploadFromUri( uri = avatarUri, filename = UUID.randomUUID().toString(), - mimeType = MimeTypes.Jpeg) + mimeType = MimeTypes.Jpeg + ) } }?.let { response -> Event( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 9bd15a0267..6dd2c91048 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelpe import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -56,7 +57,8 @@ internal class DefaultCreateRoomTask @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, private val createRoomBodyBuilder: CreateRoomBodyBuilder, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : CreateRoomTask { override suspend fun execute(params: CreateRoomParams): String { @@ -106,7 +108,7 @@ internal class DefaultCreateRoomTask @Inject constructor( } awaitTransaction(realmConfiguration) { - RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis() } if (otherUserId != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 70ba9287a2..d3d1cb856a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -61,7 +62,8 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( private val roomMemberEventHandler: RoomMemberEventHandler, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val deviceListManager: DeviceListManager, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -107,7 +109,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) - val now = System.currentTimeMillis() + val now = clock.epochMillis() for (roomMemberEvent in response.roomMemberEvents) { if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) { continue diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt index 85300fa351..a1b30a0be5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt @@ -83,7 +83,8 @@ internal class RoomMemberEventHandler @Inject constructor( roomMember, // When an update is happening, insertOrUpdate replace existing values with null if they are not provided, // but we want to preserve presence record value and not replace it with null - getExistingPresenceState(realm, roomId, userId)) + getExistingPresenceState(realm, roomId, userId) + ) realm.insertOrUpdate(roomMemberEntity) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index f883cc33ec..4c631c2182 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure @@ -35,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.time.Clock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -52,8 +54,10 @@ internal class DefaultJoinRoomTask @Inject constructor( private val readMarkersTask: SetReadMarkersTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val coroutineDispatcher: MatrixCoroutineDispatchers, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : JoinRoomTask { override suspend fun execute(params: JoinRoomTask.Params) { @@ -90,7 +94,7 @@ internal class DefaultJoinRoomTask @Inject constructor( throw JoinRoomFailure.JoinedWithTimeout } awaitTransaction(realmConfiguration) { - RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis() } setReadMarkers(roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index fe7dc28228..e926d6a785 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -148,7 +148,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr EventType.STATE_ROOM_MEMBER -> listOf("membership") EventType.STATE_ROOM_CREATE -> listOf("creator") EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") - EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + EventType.STATE_ROOM_POWER_LEVELS -> listOf( + "users", "users_default", "events", "events_default", @@ -156,7 +157,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr "ban", "kick", "redact", - "invite") + "invite" + ) EventType.STATE_ROOM_ALIASES -> listOf("aliases") EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") EventType.FEEDBACK -> listOf("type", "target_event_id") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index 64f1cc34f1..a124a8a4c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHand import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject import kotlin.collections.set @@ -58,7 +59,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor( private val roomFullyReadHandler: RoomFullyReadHandler, private val readReceiptHandler: ReadReceiptHandler, @UserId private val userId: String, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + private val clock: Clock, ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -125,7 +127,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) } if (readReceiptId != null) { - val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis()) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null) } if (shouldUpdateRoomSummary) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index ab514d31c8..7b68e2a74c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -131,7 +131,8 @@ internal class DefaultRelationService @AssistedInject constructor( replyText = replyText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId, - showInThread = showInThread) + showInThread = showInThread + ) ?.also { saveLocalEcho(it) } ?: return null @@ -186,7 +187,8 @@ internal class DefaultRelationService @AssistedInject constructor( text = replyInThreadText, msgType = msgType, autoMarkdown = autoMarkdown, - formattedText = formattedText) + formattedText = formattedText + ) .also { saveLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index b54cd71e50..7bf7d6b587 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -27,12 +27,16 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor, - private val eventFactory: LocalEchoEventFactory, - private val localEchoRepository: LocalEchoRepository) { +internal class EventEditor @Inject constructor( + private val eventSenderProcessor: EventSenderProcessor, + private val eventFactory: LocalEchoEventFactory, + private val localEchoRepository: LocalEchoRepository, + private val clock: Clock, +) { fun editTextMessage(targetEvent: TimelineEvent, msgType: String, @@ -126,7 +130,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: } private fun updateFailedEchoWithEvent(roomId: String, failedEchoEventId: String, editedEvent: Event) { - val editedEventEntity = editedEvent.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val editedEventEntity = editedEvent.toEntity(roomId, SendState.UNSENT, clock.epochMillis()) localEchoRepository.updateEchoAsync(failedEchoEventId) { _, entity -> entity.content = editedEventEntity.content entity.ageLocalTs = editedEventEntity.ageLocalTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index b596f2288e..c5f9bd13fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -55,12 +56,14 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val cryptoService: DefaultCryptoService, @UserId private val userId: String, + private val clock: Clock, ) : FetchThreadSummariesTask { override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { val filter = FilterFactory.createThreadsFilter( numberOfEvents = params.limit, - userId = if (params.isUserParticipating) userId else null).toJSONString() + userId = if (params.isUserParticipating) userId else null + ).toJSONString() val response = executeRequest( globalErrorReceiver, @@ -94,7 +97,9 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( roomMemberContentsByUser = roomMemberContentsByUser, roomEntity = roomEntity, userId = userId, - cryptoService = cryptoService) + cryptoService = cryptoService, + currentTimeMillis = clock.epochMillis(), + ) } } return Result.SUCCESS diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 116d4aa0a1..8d35a8fea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -42,7 +41,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent @@ -51,6 +49,7 @@ import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -85,10 +84,9 @@ internal interface FetchThreadTimelineTask : Task + rootThreadEventId = rootThreadId + ).also { event -> createLocalEcho(event) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 0d75a375cc..d019ffada6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -70,6 +70,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils +import org.matrix.android.sdk.internal.util.time.Clock import java.util.UUID import javax.inject.Inject @@ -90,7 +91,8 @@ internal class LocalEchoEventFactory @Inject constructor( private val thumbnailExtractor: ThumbnailExtractor, private val waveformSanitizer: WaveFormSanitizer, private val localEchoRepository: LocalEchoRepository, - private val permalinkFactory: PermalinkFactory + private val permalinkFactory: PermalinkFactory, + private val clock: Clock, ) { fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { @@ -242,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor( body = geoUri, unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri), unstableLocationAsset = LocationAsset(type = assetType), - unstableTimestampMillis = System.currentTimeMillis(), + unstableTimestampMillis = clock.epochMillis(), unstableText = geoUri ) return createMessageEvent(roomId, content) @@ -261,7 +263,7 @@ internal class LocalEchoEventFactory @Inject constructor( eventId = beaconInfoEventId ), unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri), - unstableTimestampMillis = System.currentTimeMillis(), + unstableTimestampMillis = clock.epochMillis(), ) val localId = LocalEcho.createLocalEchoId() return Event( @@ -547,7 +549,7 @@ internal class LocalEchoEventFactory @Inject constructor( } private fun dummyOriginServerTs(): Long { - return System.currentTimeMillis() + return clock.epochMillis() } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 1b1a66a1c4..9fd45b917f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -43,16 +43,20 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.UUID import javax.inject.Inject -internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, - private val realmSessionProvider: RealmSessionProvider, - private val roomSummaryUpdater: RoomSummaryUpdater, - private val timelineInput: TimelineInput, - private val timelineEventMapper: TimelineEventMapper) { +internal class LocalEchoRepository @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val realmSessionProvider: RealmSessionProvider, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val timelineInput: TimelineInput, + private val timelineEventMapper: TimelineEventMapper, + private val clock: Clock, +) { fun createLocalEcho(event: Event) { val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") @@ -61,7 +65,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private event.type ?: throw IllegalStateException("You should have set a type for your event") val timelineEventEntity = realmSessionProvider.withRealm { realm -> - val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val eventEntity = event.toEntity(roomId, SendState.UNSENT, clock.epochMillis()) val roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) val localId = UUID.randomUUID().mostSignificantBits @@ -88,7 +92,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) { - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") + Timber.v("## SendEvent: [${clock.epochMillis()}] Update local state of $eventId to ${sendState.name}") timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState) updateEchoAsync(eventId) { realm, sendingEventEntity -> if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index a54ce833d4..ecc8149255 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -77,7 +77,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo val roomId = localEchoIds.roomId val eventId = localEchoIds.eventId localEchoRepository.updateSendState(eventId, roomId, SendState.SENDING) - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId") + Timber.v("## SendEvent: Schedule send event $eventId") val sendWork = createSendEventWork(params.sessionId, eventId, true) timelineSendEventWorkCommon.postWork(roomId, sendWork) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index 83c61d2845..db8d1b5674 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -74,9 +74,13 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses else -> { // TODO mark as failed to send? // always return success, or the chain will be stuck for ever! - Result.success(WorkerParamsFactory.toData(params.copy( - lastFailureMessage = it.localizedMessage - ))) + Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = it.localizedMessage + ) + ) + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 669891c822..ddbe8a61a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -89,13 +89,13 @@ internal class SendEventWorker(context: Context, params: WorkerParameters, sessi .also { Timber.e("Work cancelled due to input error from parent") } } - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") + Timber.v("## SendEvent: Send event ${params.eventId}") return try { sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId))) Result.success() } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { - Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") + Timber.e("## SendEvent: Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") localEchoRepository.updateSendState( eventId = event.eventId, roomId = event.roomId, @@ -104,7 +104,7 @@ internal class SendEventWorker(context: Context, params: WorkerParameters, sessi ) Result.success() } else { - Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") + Timber.e("## SendEvent: Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Result.retry() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt index 8caa99d90a..49bc05f40c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt @@ -54,7 +54,8 @@ internal fun TextContent.toThreadTextContent( isFallingBack = true, inReplyTo = ReplyToContent( eventId = latestThreadEventId - )), + ) + ), formattedBody = formattedText ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 5b4efa5df6..a1d3e2c0ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -191,7 +191,7 @@ internal class EventSenderProcessorCoroutine @Inject constructor( private suspend fun QueuedTask.waitForNetwork() = waitForNetworkSequencer.post { while (!canReachServer.get()) { - Timber.v("## $this cannot reach server wait ts:${System.currentTimeMillis()}") + Timber.v("## $this cannot reach server wait for $$RETRY_WAIT_TIME_MS ms") delay(RETRY_WAIT_TIME_MS) withContext(Dispatchers.IO) { val hostAvailable = HomeServerAvailabilityChecker(sessionParams).check() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt index 1ee3139194..301f8cb9d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt @@ -136,7 +136,7 @@ internal class EventSenderProcessorThread @Inject constructor( private var retryNoNetworkTask: TimerTask? = null override fun run() { - Timber.v("## SendThread started ts:${System.currentTimeMillis()}") + Timber.v("## SendThread started") try { while (!isInterrupted) { Timber.v("## SendThread wait for task to process") @@ -151,7 +151,7 @@ internal class EventSenderProcessorThread @Inject constructor( } // we check for network connectivity while (!canReachServer) { - Timber.v("## SendThread cannot reach server, wait ts:${System.currentTimeMillis()}") + Timber.v("## SendThread cannot reach server") // schedule to retry waitForNetwork() // if thread as been killed meanwhile @@ -175,7 +175,7 @@ internal class EventSenderProcessorThread @Inject constructor( canReachServer = false if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() while (!canReachServer) { - Timber.v("## SendThread retryLoop cannot reach server, wait ts:${System.currentTimeMillis()}") + Timber.v("## SendThread retryLoop cannot reach server") // schedule to retry waitForNetwork() } @@ -218,7 +218,7 @@ internal class EventSenderProcessorThread @Inject constructor( // state = State.KILLED // is this needed? retryNoNetworkTask?.cancel() - Timber.w("## SendThread finished ${System.currentTimeMillis()}") + Timber.w("## SendThread finished") } private fun waitForNetwork() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt index a03ab778f6..a7863470f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt @@ -37,9 +37,10 @@ internal interface TaskInfo { const val TYPE_REDACT = "TYPE_REDACT" private val moshi = Moshi.Builder() - .add(RuntimeJsonAdapterFactory.of(TaskInfo::class.java, "type", FallbackTaskInfo::class.java) - .registerSubtype(SendEventTaskInfo::class.java, TYPE_SEND) - .registerSubtype(RedactEventTaskInfo::class.java, TYPE_REDACT) + .add( + RuntimeJsonAdapterFactory.of(TaskInfo::class.java, "type", FallbackTaskInfo::class.java) + .registerSubtype(SendEventTaskInfo::class.java, TYPE_SEND) + .registerSubtype(RedactEventTaskInfo::class.java, TYPE_REDACT) ) .add(SerializeNulls.JSON_ADAPTER_FACTORY) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index b65991347d..6c6d6368d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -72,17 +72,21 @@ internal class DefaultThreadsService @AssistedInject constructor( } override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) { - fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( - roomId = roomId, - rootThreadEventId = rootThreadEventId, - from = from, - limit = limit - )) + fetchThreadTimelineTask.execute( + FetchThreadTimelineTask.Params( + roomId = roomId, + rootThreadEventId = rootThreadEventId, + from = from, + limit = limit + ) + ) } override suspend fun fetchThreadSummaries() { - fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params( - roomId = roomId - )) + fetchThreadSummariesTask.execute( + FetchThreadSummariesTask.Params( + roomId = roomId + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt index 3bc36fb2a8..296981dfc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt @@ -83,7 +83,8 @@ internal class DefaultThreadsLocalService @AssistedInject constructor( realm = it, roomId = roomId, rootThreadEventId = rootThreadEventId, - senderId = userId) + senderId = userId + ) } } @@ -97,7 +98,8 @@ internal class DefaultThreadsLocalService @AssistedInject constructor( monarchy.awaitTransaction { EventEntity.where( realm = it, - eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE + eventId = rootThreadEventId + ).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 08b2700a43..ac2dfb101c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -43,28 +43,32 @@ import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHand import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.createBackgroundHandler +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -internal class DefaultTimeline(private val roomId: String, - private val initialEventId: String?, - private val realmConfiguration: RealmConfiguration, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val readReceiptHandler: ReadReceiptHandler, - private val settings: TimelineSettings, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - paginationTask: PaginationTask, - getEventTask: GetContextOfEventTask, - fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - fetchThreadTimelineTask: FetchThreadTimelineTask, - timelineEventMapper: TimelineEventMapper, - timelineInput: TimelineInput, - threadsAwarenessHandler: ThreadsAwarenessHandler, - lightweightSettingsStorage: LightweightSettingsStorage, - eventDecryptor: TimelineEventDecryptor) : Timeline { +internal class DefaultTimeline( + private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + private val settings: TimelineSettings, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + fetchThreadTimelineTask: FetchThreadTimelineTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + threadsAwarenessHandler: ThreadsAwarenessHandler, + lightweightSettingsStorage: LightweightSettingsStorage, + eventDecryptor: TimelineEventDecryptor, +) : Timeline { companion object { val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread") @@ -287,7 +291,6 @@ internal class DefaultTimeline(private val roomId: String, } } - @Suppress("EXPERIMENTAL_API_USAGE") private fun listenToPostSnapshotSignals() { postSnapshotSignalFlow .sample(150) @@ -370,7 +373,8 @@ internal class DefaultTimeline(private val roomId: String, roomId = roomId, timelineId = timelineID, mode = mode, - dependencies = strategyDependencies + dependencies = strategyDependencies, + clock = clock, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 826c9d7c48..6d63b24cf5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTa import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import org.matrix.android.sdk.internal.util.time.Clock internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, @@ -50,7 +51,8 @@ internal class DefaultTimelineService @AssistedInject constructor( private val lightweightSettingsStorage: LightweightSettingsStorage, private val readReceiptHandler: ReadReceiptHandler, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val timelineEventDataSource: TimelineEventDataSource + private val timelineEventDataSource: TimelineEventDataSource, + private val clock: Clock, ) : TimelineService { @AssistedFactory @@ -75,7 +77,8 @@ internal class DefaultTimelineService @AssistedInject constructor( readReceiptHandler = readReceiptHandler, getEventTask = contextOfEventTask, threadsAwarenessHandler = threadsAwarenessHandler, - lightweightSettingsStorage = lightweightSettingsStorage + lightweightSettingsStorage = lightweightSettingsStorage, + clock = clock ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index 5bca5118b8..aef9e24c8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal interface GetEventTask : Task { @@ -36,7 +37,8 @@ internal interface GetEventTask : Task { internal class DefaultGetEventTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, - private val eventDecryptor: EventDecryptor + private val eventDecryptor: EventDecryptor, + private val clock: Clock, ) : GetEventTask { override suspend fun execute(params: GetEventTask.Params): Event { @@ -59,7 +61,7 @@ internal class DefaultGetEventTask @Inject constructor( } } - event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + event.ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } return event } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 8819ffe69f..bcf202962c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThre import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.concurrent.atomic.AtomicReference @@ -53,11 +54,13 @@ import java.util.concurrent.atomic.AtomicReference * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. */ -internal class LoadTimelineStrategy( +internal class LoadTimelineStrategy constructor( private val roomId: String, private val timelineId: String, private val mode: Mode, - private val dependencies: Dependencies) { + private val dependencies: Dependencies, + clock: Clock, +) { sealed interface Mode { object Live : Mode @@ -153,7 +156,7 @@ internal class LoadTimelineStrategy( } } - private val uiEchoManager = UIEchoManager(uiEchoManagerListener) + private val uiEchoManager = UIEchoManager(uiEchoManagerListener, clock) private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( roomId = roomId, realm = dependencies.realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 27f4245b22..8541c478ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -180,12 +180,14 @@ internal class TimelineChunk( val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE return if (direction == Timeline.Direction.BACKWARDS) { try { - fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params( - roomId, - rootThreadEventId, - chunkEntity.prevToken, - count - )).toLoadMoreResult() + fetchThreadTimelineTask.execute( + FetchThreadTimelineTask.Params( + roomId, + rootThreadEventId, + chunkEntity.prevToken, + count + ) + ).toLoadMoreResult() } catch (failure: Throwable) { Timber.e(failure, "Failed to fetch thread timeline events from the server") LoadMoreResult.FAILURE @@ -239,10 +241,12 @@ internal class TimelineChunk( * Simple log that displays the number and timeline of loaded events */ private fun logLoadedFromStorage(loadedFromStorage: LoadedFromStorage, direction: Timeline.Direction) { - Timber.v("[" + - "${if (timelineSettings.isThreadTimeline()) "ThreadTimeLine" else "Timeline"}] Has loaded " + - "${loadedFromStorage.numberOfEvents} items from storage in $direction " + - if (timelineSettings.isThreadTimeline() && loadedFromStorage.threadReachedEnd) "[Reached End]" else "") + Timber.v( + "[" + + "${if (timelineSettings.isThreadTimeline()) "ThreadTimeLine" else "Timeline"}] Has loaded " + + "${loadedFromStorage.numberOfEvents} items from storage in $direction " + + if (timelineSettings.isThreadTimeline() && loadedFromStorage.threadReachedEnd) "[Reached End]" else "" + ) } fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { @@ -361,7 +365,8 @@ internal class TimelineChunk( } return LoadedFromStorage( threadReachedEnd = threadReachedEnd(timelineEvents), - numberOfEvents = timelineEvents.size) + numberOfEvents = timelineEvents.size + ) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index d3f24a8568..96ceb6c6dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -54,7 +55,9 @@ internal class TokenChunkEventPersistor @Inject constructor( @SessionDatabase private val monarchy: Monarchy, @UserId private val userId: String, private val lightweightSettingsStorage: LightweightSettingsStorage, - private val liveEventManager: Lazy) { + private val liveEventManager: Lazy, + private val clock: Clock, +) { enum class Result { SHOULD_FETCH_MORE, @@ -166,7 +169,7 @@ internal class TokenChunkEventPersistor @Inject constructor( val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents - val now = System.currentTimeMillis() + val now = clock.epochMillis() stateEvents?.forEach { stateEvent -> val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } @@ -227,7 +230,8 @@ internal class TokenChunkEventPersistor @Inject constructor( roomId = roomId, eventEntity = eventEntity, direction = direction, - roomMemberContentsByUser = roomMemberContentsByUser) + roomMemberContentsByUser = roomMemberContentsByUser + ) if (lightweightSettingsStorage.areThreadMessagesEnabled()) { eventEntity.rootThreadEventId?.let { // This is a thread event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index f3218173d6..828e01955a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -24,10 +24,14 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.Collections -internal class UIEchoManager(private val listener: Listener) { +internal class UIEchoManager( + private val listener: Listener, + private val clock: Clock, +) { interface Listener { fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean @@ -109,7 +113,7 @@ internal class UIEchoManager(private val listener: Listener) { key = uiEchoReaction.reaction, count = 1, addedByMe = true, - firstTimestamp = System.currentTimeMillis(), + firstTimestamp = clock.epochMillis(), sourceEvents = emptyList(), localEchoEvents = listOf(uiEchoReaction.localEchoId) ).let { updateReactions.add(it) } @@ -143,7 +147,7 @@ internal class UIEchoManager(private val listener: Listener) { fun updateSentStateWithUiEcho(timelineEvent: TimelineEvent): TimelineEvent { if (timelineEvent.root.sendState.isSent()) return timelineEvent val inMemoryState = inMemorySendingStates[timelineEvent.eventId] ?: return timelineEvent - // Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state $inMemoryState from state ${element.root.sendState}") + // Timber.v("## ${clock.epochMillis()} Send event refresh echo with live state $inMemoryState from state ${element.root.sendState}") return timelineEvent.copy( root = timelineEvent.root.copyAll() .also { it.sendState = inMemoryState } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt index 8033b0654d..12ca36fa6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt @@ -32,15 +32,17 @@ internal class DefaultSearchService @Inject constructor( beforeLimit: Int, afterLimit: Int, includeProfile: Boolean): SearchResult { - return searchTask.execute(SearchTask.Params( - searchTerm = searchTerm, - roomId = roomId, - nextBatch = nextBatch, - orderByRecent = orderByRecent, - limit = limit, - beforeLimit = beforeLimit, - afterLimit = afterLimit, - includeProfile = includeProfile - )) + return searchTask.execute( + SearchTask.Params( + searchTerm = searchTerm, + roomId = roomId, + nextBatch = nextBatch, + orderByRecent = orderByRecent, + limit = limit, + beforeLimit = beforeLimit, + afterLimit = afterLimit, + includeProfile = includeProfile + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index 17dc90fdb0..267023d186 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -169,8 +169,10 @@ internal class SecretStoringUtils @Inject constructor( if (secretKeyEntry == null) { // we generate it val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") - val keyGenSpec = KeyGenParameterSpec.Builder(alias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + val keyGenSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(128) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 7eeaed4ff6..a0925cabcc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -49,11 +49,13 @@ internal class DefaultJoinSpaceTask @Inject constructor( override suspend fun execute(params: JoinSpaceTask.Params): JoinSpaceResult { Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...") try { - joinRoomTask.execute(JoinRoomTask.Params( - params.roomIdOrAlias, - params.reason, - params.viaServers - )) + joinRoomTask.execute( + JoinRoomTask.Params( + params.roomIdOrAlias, + params.reason, + params.viaServers + ) + ) } catch (failure: Throwable) { return JoinSpaceResult.Fail(failure) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt index bd20ada28b..8e0c3422b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.JsonClass import okio.buffer import okio.source import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.io.File @@ -46,7 +47,10 @@ internal interface InitialSyncStatusRepository { /** * This class handle the current status of an initial sync and persist it on the disk, to be robust against crash */ -internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncStatusRepository { +internal class FileInitialSyncStatusRepository( + directory: File, + private val clock: Clock, +) : InitialSyncStatusRepository { companion object { // After 2 hours, we consider that the downloaded file is outdated: @@ -64,7 +68,7 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta ensureCache() val state = cache?.step ?: InitialSyncStatus.STEP_INIT return if (state >= InitialSyncStatus.STEP_DOWNLOADED && - System.currentTimeMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { + clock.epochMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { Timber.d("INIT_SYNC downloaded file is outdated, download it again") // The downloaded file is outdated setStep(InitialSyncStatus.STEP_INIT) @@ -79,7 +83,7 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta if (step == InitialSyncStatus.STEP_DOWNLOADED) { // Also store the downloaded date newStatus = newStatus.copy( - downloadedDate = System.currentTimeMillis() + downloadedDate = clock.epochMillis() ) } cache = newStatus diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 197037f1cf..f88d973101 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseP import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.logDuration +import org.matrix.android.sdk.internal.util.time.Clock import retrofit2.Response import retrofit2.awaitResponse import timber.log.Timber @@ -78,11 +79,12 @@ internal class DefaultSyncTask @Inject constructor( @SessionFilesDirectory private val fileDirectory: File, private val syncResponseParser: InitialSyncResponseParser, - private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore + private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, + private val clock: Clock, ) : SyncTask { private val workingDir = File(fileDirectory, "is") - private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir) + private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir, clock) override suspend fun execute(params: SyncTask.Params): SyncResponse { return syncTaskSequencer.post { @@ -111,7 +113,8 @@ internal class DefaultSyncTask @Inject constructor( userStore.createOrUpdate( userId = userId, displayName = user?.displayName, - avatarUrl = user?.avatarUrl) + avatarUrl = user?.avatarUrl + ) defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100) } // Maybe refresh the homeserver capabilities data we know @@ -124,7 +127,7 @@ internal class DefaultSyncTask @Inject constructor( if (isInitialSync) { Timber.tag(loggerTag.value).d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy - logDuration("INIT_SYNC strategy: $initSyncStrategy", loggerTag) { + logDuration("INIT_SYNC strategy: $initSyncStrategy", loggerTag, clock) { if (initSyncStrategy is InitialSyncStrategy.Optimized) { roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() @@ -135,7 +138,7 @@ internal class DefaultSyncTask @Inject constructor( // Delete all files workingDir.deleteRecursively() } else { - val syncResponse = logDuration("INIT_SYNC Request", loggerTag) { + val syncResponse = logDuration("INIT_SYNC Request", loggerTag, clock) { executeRequest(globalErrorReceiver) { syncAPI.sync( params = requestParams, @@ -146,7 +149,7 @@ internal class DefaultSyncTask @Inject constructor( // We cannot distinguish request and download in this case. syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() syncStatisticsData.downloadInitSyncTime = syncStatisticsData.requestInitSyncTime - logDuration("INIT_SYNC Database insertion", loggerTag) { + logDuration("INIT_SYNC Database insertion", loggerTag, clock) { syncResponseHandler.handleResponse(syncResponse, token, defaultSyncStatusService) } syncResponseToReturn = syncResponse @@ -174,10 +177,12 @@ internal class DefaultSyncTask @Inject constructor( Timber.tag(loggerTag.value).d( "Incremental sync request parsing, $nbRooms room(s) $nbToDevice toDevice(s). Got nextBatch: $nextBatch" ) - defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncParsing( - rooms = nbRooms, - toDevice = nbToDevice - )) + defaultSyncStatusService.setStatus( + SyncStatusService.Status.IncrementalSyncParsing( + rooms = nbRooms, + toDevice = nbToDevice + ) + ) syncResponseHandler.handleResponse(syncResponse, token, null) syncResponseToReturn = syncResponse Timber.tag(loggerTag.value).d("Incremental sync done") @@ -201,14 +206,14 @@ internal class DefaultSyncTask @Inject constructor( } } else { initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADING) - val syncResponse = logDuration("INIT_SYNC Perform server request", loggerTag) { + val syncResponse = logDuration("INIT_SYNC Perform server request", loggerTag, clock) { reportSubtask(defaultSyncStatusService, InitSyncStep.ServerComputing, 1, 0.2f) { getSyncResponse(requestParams, MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT) } } syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() if (syncResponse.isSuccessful) { - logDuration("INIT_SYNC Download and save to file", loggerTag) { + logDuration("INIT_SYNC Download and save to file", loggerTag, clock) { reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.1f) { syncResponse.body()?.byteStream()?.use { inputStream -> workingFile.outputStream().use { outputStream -> @@ -247,8 +252,8 @@ internal class DefaultSyncTask @Inject constructor( } private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse { - return logDuration("INIT_SYNC handleSyncFile()", loggerTag) { - val syncResponse = logDuration("INIT_SYNC Read file and parse", loggerTag) { + return logDuration("INIT_SYNC handleSyncFile()", loggerTag, clock) { + val syncResponse = logDuration("INIT_SYNC Read file and parse", loggerTag, clock) { syncResponseParser.parse(initSyncStrategy, workingFile) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_PARSED) @@ -257,7 +262,7 @@ internal class DefaultSyncTask @Inject constructor( val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it.ephemeral is LazyRoomSyncEphemeral.Stored } Timber.tag(loggerTag.value).d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files") - logDuration("INIT_SYNC Database insertion", loggerTag) { + logDuration("INIT_SYNC Database insertion", loggerTag, clock) { syncResponseHandler.handleResponse(syncResponse, null, defaultSyncStatusService) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index 2c84f1e692..77bee18df9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -44,12 +44,14 @@ internal class ReadReceiptHandler @Inject constructor( companion object { - fun createContent(userId: String, eventId: String): ReadReceiptContent { + fun createContent(userId: String, + eventId: String, + currentTimeMillis: Long): ReadReceiptContent { return mapOf( eventId to mapOf( READ_KEY to mapOf( userId to mapOf( - TIMESTAMP_KEY to System.currentTimeMillis().toDouble() + TIMESTAMP_KEY to currentTimeMillis.toDouble() ) ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 05dad983da..a3be8b56a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -79,22 +79,26 @@ import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.util.computeBestChunkSize +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, - private val roomSummaryUpdater: RoomSummaryUpdater, - private val roomAccountDataHandler: RoomSyncAccountDataHandler, - private val cryptoService: DefaultCryptoService, - private val roomMemberEventHandler: RoomMemberEventHandler, - private val roomTypingUsersHandler: RoomTypingUsersHandler, - private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, - @UserId private val userId: String, - private val homeServerCapabilitiesService: HomeServerCapabilitiesService, - private val lightweightSettingsStorage: LightweightSettingsStorage, - private val timelineInput: TimelineInput, - private val liveEventService: Lazy) { +internal class RoomSyncHandler @Inject constructor( + private val readReceiptHandler: ReadReceiptHandler, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomAccountDataHandler: RoomSyncAccountDataHandler, + private val cryptoService: DefaultCryptoService, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + @UserId private val userId: String, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val lightweightSettingsStorage: LightweightSettingsStorage, + private val timelineInput: TimelineInput, + private val liveEventService: Lazy, + private val clock: Clock, +) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -130,7 +134,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } else { EventInsertType.INCREMENTAL_SYNC } - val syncLocalTimeStampMillis = System.currentTimeMillis() + val syncLocalTimeStampMillis = clock.epochMillis() val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> { if (isInitialSync && initialSyncStrategy is InitialSyncStrategy.Optimized) { @@ -378,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val roomMemberContentsByUser = HashMap() val optimizedThreadSummaryMap = hashMapOf() - for (event in eventList) { + for (rawEvent in eventList) { + // It's annoying roomId is not there, but lot of code rely on it. + // And had to do it now as copy would delete all decryption results.. + val event = rawEvent.copy(roomId = roomId) if (event.eventId == null || event.senderId == null || event.type == null) { continue } @@ -440,7 +447,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle threadEventEntity = eventEntity, roomMemberContentsByUser = roomMemberContentsByUser, userId = userId, - roomEntity = roomEntity + roomEntity = roomEntity, + currentTimeMillis = clock.epochMillis(), ) } } ?: run { @@ -449,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event) + cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo event.unsignedData?.transactionId?.also { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index efc8e39a18..7b7df57bc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -186,7 +186,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( eventBody = eventBody, eventToInject = eventToInject, eventToInjectBody = eventToInjectBody, - threadRelation = threadRelation) ?: return null + threadRelation = threadRelation + ) ?: return null // update the event contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent) @@ -253,7 +254,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( eventBody = newEventBody, eventToInject = event, eventToInjectBody = eventBody, - threadRelation = threadRelation) ?: return null + threadRelation = threadRelation + ) ?: return null return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent) } @@ -309,7 +311,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( userLink, eventIdToInjectSenderId, eventToInjectBody, - eventBody) + eventBody + ) return MessageTextContent( relatesTo = threadRelation, @@ -330,7 +333,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( threadRelation: RelationDefaultContent?): String? { val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format( "In reply to a thread", - eventBody) + eventBody + ) val messageTextContent = MessageTextContent( relatesTo = threadRelation, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index 5f62f40ab3..5d2fa0fc5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -108,9 +108,11 @@ internal class DefaultTermsService @Inject constructor( val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( - acceptedTermsContent = AcceptedTermsContent(newList) - )) + updateUserAccountDataTask.execute( + UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + ) + ) } private suspend fun getToken(url: String): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt index 874c2741de..c4ea029cbb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt @@ -60,8 +60,10 @@ internal class DefaultUpdateBreadcrumbsTask @Inject constructor( // FIXME It can remove the previous breadcrumbs, if not synced yet // And update account data - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams( - breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs) - )) + updateUserAccountDataTask.execute( + UpdateUserAccountDataTask.BreadcrumbsParams( + breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs) + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt index 6fd907d397..ffad0b856c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.util import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber internal fun Collection.logLimit(maxQuantity: Int = 5): String { @@ -34,13 +35,14 @@ internal fun Collection.logLimit(maxQuantity: Int = 5): String { internal suspend fun logDuration(message: String, loggerTag: LoggerTag, + clock: Clock, block: suspend () -> T): T { Timber.tag(loggerTag.value).d("$message -- BEGIN") - val start = System.currentTimeMillis() + val start = clock.epochMillis() val result = logRamUsage(message) { block() } - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.tag(loggerTag.value).d("$message -- END duration: $duration ms") return result diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt index 8a7b50175a..396d12f369 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -18,10 +18,15 @@ package org.matrix.android.sdk.internal.util.system import dagger.Binds import dagger.Module +import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.android.sdk.internal.util.time.DefaultClock @Module internal abstract class SystemModule { @Binds abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider + + @Binds + abstract fun bindClock(clock: DefaultClock): Clock } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt similarity index 53% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt index 97c369db3e..4fe9069b49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/time/Clock.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,23 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto +package org.matrix.android.sdk.internal.util.time -internal interface IncomingShareRequestCommon { - /** - * The user id - */ - val userId: String? +import javax.inject.Inject - /** - * The device id - */ - val deviceId: String? - - /** - * The request id - */ - val requestId: String? - - val localCreationTimestamp: Long? +internal interface Clock { + fun epochMillis(): Long +} + +internal class DefaultClock @Inject constructor() : Clock { + + /** + * Provides a UTC epoch in milliseconds + * + * This value is not guaranteed to be correct with reality + * as a User can override the system time and date to any values. + */ + override fun epochMillis(): Long { + return System.currentTimeMillis() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index 82ff9a321f..a6b398f6f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -43,7 +43,7 @@ internal interface GetWellknownTask : Task AddPusherWorker(appContext, workerParameters, sessionManager) - CancelGossipRequestWorker::class.java.name -> - CancelGossipRequestWorker(appContext, workerParameters, sessionManager) GetGroupDataWorker::class.java.name -> GetGroupDataWorker(appContext, workerParameters, sessionManager) MultipleEventSendingDispatcherWorker::class.java.name -> @@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage RedactEventWorker(appContext, workerParameters, sessionManager) SendEventWorker::class.java.name -> SendEventWorker(appContext, workerParameters, sessionManager) - SendGossipRequestWorker::class.java.name -> - SendGossipRequestWorker(appContext, workerParameters, sessionManager) - SendGossipWorker::class.java.name -> - SendGossipWorker(appContext, workerParameters, sessionManager) - SendVerificationMessageWorker::class.java.name -> - SendVerificationMessageWorker(appContext, workerParameters, sessionManager) SyncWorker::class.java.name -> SyncWorker(appContext, workerParameters, sessionManager) UpdateTrustWorker::class.java.name -> @@ -96,9 +84,11 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage CoroutineWorker(context, workerParameters) { // Called by WorkManager if there is no MatrixWorkerFactory - constructor(context: Context, workerParameters: WorkerParameters) : this(context, + constructor(context: Context, workerParameters: WorkerParameters) : this( + context, workerParameters, - isCreatedByMatrixWorkerFactory = false) + isCreatedByMatrixWorkerFactory = false + ) override suspend fun doWork(): Result { return if (!isCreatedByMatrixWorkerFactory) { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt index 088e160950..e1438f4741 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -62,12 +62,12 @@ class VersionsKtTest { Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false - Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true - Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true - Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false - Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt index 95787173da..c4a3404e80 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/pushrules/PushRulesConditionTest.kt @@ -43,7 +43,8 @@ class PushRulesConditionTest : MatrixTest { type = "m.room.message", eventId = "mx0", content = MessageTextContent("m.text", text).toContent(), - originServerTs = 0) + originServerTs = 0 + ) } @Test @@ -62,7 +63,8 @@ class PushRulesConditionTest : MatrixTest { eventId = "mx0", stateKey = "@foo:matrix.org", content = rm.toContent(), - originServerTs = 0) + originServerTs = 0 + ) assert(condition.isSatisfied(simpleTextEvent)) assert(!condition.isSatisfied(simpleRoomMemberEvent)) @@ -131,7 +133,8 @@ class PushRulesConditionTest : MatrixTest { eventId = "mx0", content = MessageTextContent("m.notice", "A").toContent(), originServerTs = 0, - roomId = "2joined").also { + roomId = "2joined" + ).also { assertTrue("Notice", conditionEqual.isSatisfied(it)) } } @@ -175,7 +178,8 @@ class PushRulesConditionTest : MatrixTest { eventId = "mx0", content = MessageTextContent("m.text", "A").toContent(), originServerTs = 0, - roomId = room2JoinedId).also { + roomId = room2JoinedId + ).also { assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) @@ -186,7 +190,8 @@ class PushRulesConditionTest : MatrixTest { eventId = "mx0", content = MessageTextContent("m.text", "A").toContent(), originServerTs = 0, - roomId = room3JoinedId).also { + roomId = room3JoinedId + ).also { assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) @@ -206,7 +211,8 @@ class PushRulesConditionTest : MatrixTest { eventId = "mx0", content = MessageTextContent("m.text", "How was the cake benoit?").toContent(), originServerTs = 0, - roomId = "2joined") + roomId = "2joined" + ) condition.isSatisfied(event, "how") shouldBe true condition.isSatisfied(event, "How") shouldBe true diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt index a93883a344..e6fe8fbb00 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt @@ -41,13 +41,17 @@ class Base58Test : MatrixTest { @Test fun encode_curve25519() { // Encode a 32 bytes key - assertEquals("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr", - base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray())) + assertEquals( + "4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr", + base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray()) + ) } @Test fun decode_curve25519() { - assertArrayEquals(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(), - base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr")) + assertArrayEquals( + ("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(), + base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr") + ) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt index d4c9da2986..4146e6b3b7 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt @@ -32,7 +32,8 @@ class RecoveryKeyTest : MatrixTest { 0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(), 0x3C.toByte(), 0x16.toByte(), 0xC1.toByte(), 0x72.toByte(), 0x51.toByte(), 0xB2.toByte(), 0x66.toByte(), 0x45.toByte(), 0xDF.toByte(), 0x4C.toByte(), 0x2F.toByte(), 0x87.toByte(), 0xEB.toByte(), 0xC0.toByte(), 0x99.toByte(), 0x2A.toByte(), - 0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte()) + 0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte() + ) @Test fun isValidRecoveryKey_valid_true() { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt index 149b964fd2..0cfd9d97a8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.task +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay @@ -31,8 +33,8 @@ class CoroutineSequencersTest : MatrixTest { private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_run_sequential() { val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() @@ -60,8 +62,8 @@ class CoroutineSequencersTest : MatrixTest { assertEquals(results[2], "#3") } + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_run_parallel() { val sequencer1 = SemaphoreCoroutineSequencer() val sequencer2 = SemaphoreCoroutineSequencer() @@ -87,8 +89,8 @@ class CoroutineSequencersTest : MatrixTest { assertEquals(3, results.size) } + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_jump_to_next_when_current_job_canceled() { val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 21ab0bab77..393e942b2a 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -173,4 +173,7 @@ PreferenceManager\.getDefaultSharedPreferences==2 # findViewById ### Do not use `template_` string. Please remove the prefix `template_` to use the generated resource instead. -R\.string\.template_ \ No newline at end of file +R\.string\.template_ + +### Use the Clock interface, or use `measureTimeMillis` +System\.currentTimeMillis\(\)===2 diff --git a/tools/hs_diag.py b/tools/hs_diag.py index 50f117bc8e..8651321fa3 100755 --- a/tools/hs_diag.py +++ b/tools/hs_diag.py @@ -58,6 +58,9 @@ items = [ # Need token , ["Capability", baseUrl + "_matrix/client/r0/capabilities", True] # Need token , ["Media config", baseUrl + "_matrix/media/r0/config", True] # Need token , ["Turn", baseUrl + "_matrix/client/r0/voip/turnServer", True] + + # Only for Synapse + , ["Synapse version", baseUrl + "_synapse/admin/v1/server_version", True] ] for item in items: diff --git a/vector/build.gradle b/vector/build.gradle index 162d9fe81c..c4b1f3fa4e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 4 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 14 +ext.versionPatch = 16 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -297,14 +297,14 @@ android { kotlinOptions { jvmTarget = "11" freeCompilerArgs += [ - "-Xopt-in=kotlin.RequiresOptIn", + "-opt-in=kotlin.RequiresOptIn", // Fixes false positive "This is an internal Mavericks API. It is not intended for external use." // of MvRx `by viewModel()` calls. Maybe due to the inlining of code... This is a temporary fix... - "-Xopt-in=com.airbnb.mvrx.InternalMavericksApi", + "-opt-in=com.airbnb.mvrx.InternalMavericksApi", // Opt in for kotlinx.coroutines.FlowPreview too - "-Xopt-in=kotlinx.coroutines.FlowPreview", + "-opt-in=kotlinx.coroutines.FlowPreview", // Opt in for kotlinx.coroutines.ExperimentalCoroutinesApi too - "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ] } @@ -343,11 +343,11 @@ dependencies { implementation libs.androidx.constraintLayout implementation "androidx.sharetarget:sharetarget:1.1.0" implementation libs.androidx.core - implementation "androidx.media:media:1.4.3" + implementation "androidx.media:media:1.6.0" implementation "androidx.transition:transition:1.4.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" - implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0" + implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi kapt libs.squareup.moshiKotlin @@ -368,7 +368,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.47' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48' // FlowBinding implementation libs.github.flowBinding @@ -413,10 +413,10 @@ dependencies { implementation 'androidx.browser:browser:1.4.0' // Passphrase strength helper - implementation 'com.nulab-inc:zxcvbn:1.5.2' + implementation 'com.nulab-inc:zxcvbn:1.7.0' - // To convert voice message on old platforms - implementation 'com.arthenica:ffmpeg-kit-audio:4.5.LTS' + // To convert voice message on old platforms. Always keep the LTS suffix! + implementation 'com.arthenica:ffmpeg-kit-audio:4.5.1.LTS' // Alerter implementation 'com.github.tapadoo:alerter:7.2.4' @@ -463,7 +463,7 @@ dependencies { // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - implementation "androidx.emoji2:emoji2:1.0.1" + implementation "androidx.emoji2:emoji2:1.1.0" // WebRTC // org.webrtc:google-webrtc is for development purposes only diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 35554ae75a..efb28cdff5 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -43,6 +43,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.time.DefaultClock import im.vector.app.espresso.tools.waitUntilViewVisible import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -52,6 +53,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.util.Optional import java.util.concurrent.TimeoutException +import kotlin.random.Random object EspressoHelper { fun getCurrentActivity(): Activity? { @@ -89,6 +91,8 @@ fun getString(@StringRes id: Int): String { fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { return object : ViewAction { + private val clock = DefaultClock() + override fun getConstraints(): Matcher { return Matchers.any(View::class.java) } @@ -102,14 +106,14 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDispl override fun perform(uiController: UiController, view: View) { println("*** waitForView 1 $view") uiController.loopMainThreadUntilIdle() - val startTime = System.currentTimeMillis() + val startTime = clock.epochMillis() val endTime = startTime + timeout val visibleMatcher = isDisplayed() uiController.loopMainThreadForAtLeast(100) do { - println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}") + println("*** waitForView loop $view end:$endTime current:${clock.epochMillis()}") val viewVisible = TreeIterables.breadthFirstViewTraversal(view) .any { viewMatcher.matches(it) && visibleMatcher.matches(it) } @@ -118,7 +122,7 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDispl println("*** waitForView loop loopMainThreadForAtLeast...") uiController.loopMainThreadForAtLeast(50) println("*** waitForView loop ...loopMainThreadForAtLeast") - } while (System.currentTimeMillis() < endTime) + } while (clock.epochMillis() < endTime) println("*** waitForView timeout $view") // Timeout happens. @@ -168,9 +172,9 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource { val res = object : IdlingResource, ActivityLifecycleCallback { private var callback: IdlingResource.ResourceCallback? = null private var resumedActivity: Activity? = null - private val uniqTS = System.currentTimeMillis() + private val uniqueSuffix = Random.nextLong() - override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS" + override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqueSuffix" override fun isIdleNow(): Boolean { val activity = resumedActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).firstOrNull { diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt index fcec2a9fb5..344a2ecfb1 100644 --- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt +++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt @@ -34,6 +34,7 @@ import org.hamcrest.CoreMatchers.not import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -44,7 +45,7 @@ class RegistrationTest { @Test fun simpleRegister() { - val userId: String = "UiAutoTest_${System.currentTimeMillis()}" + val userId: String = "UiAutoTest_${Random.nextLong()}" val password: String = "password" val homeServerUrl: String = "http://10.0.2.2:8080" diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt index a84aae9994..21fd226317 100644 --- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -48,6 +48,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.matrix.android.sdk.api.session.Session +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -61,7 +62,7 @@ class SecurityBootstrapTest : VerificationTestBase() { @Before fun createSessionWithCrossSigning() { val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) stubAllExternalIntents() } diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt index 47e1e43be3..6ac3226674 100644 --- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -19,6 +19,7 @@ package im.vector.app import android.net.Uri import androidx.lifecycle.Observer import im.vector.app.ui.robot.OnboardingRobot +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -107,7 +108,7 @@ abstract class VerificationTestBase { return result!! } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) private fun syncSession(session: Session) { val lock = CountDownLatch(1) diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index c82b543a08..c970d0049f 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -66,7 +67,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() { @Before fun createSessionWithCrossSigning() { val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) doSync { existingSession!!.cryptoService().crossSigningService() @@ -81,7 +82,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() { ) ) } - }, it) + }, it + ) } } diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index 80d8315a0e..7efae073e5 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.random.Random @RunWith(AndroidJUnit4::class) @LargeTest @@ -68,7 +69,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() { fun createSessionWithCrossSigningAnd4S() { val context = InstrumentationRegistry.getInstrumentation().targetContext val matrix = getMatrixInstance() - val userName = "foobar_${System.currentTimeMillis()}" + val userName = "foobar_${Random.nextLong()}" existingSession = createAccountAndSync(matrix, userName, password, true) doSync { existingSession!!.cryptoService().crossSigningService() @@ -83,27 +84,30 @@ class VerifySessionPassphraseTest : VerificationTestBase() { ) ) } - }, it) + }, it + ) } val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources)) runBlocking { - task.execute(Params( - userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = existingSession!!.myUserId, - password = password, - session = flowResponse.session + task.execute( + Params( + userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = existingSession!!.myUserId, + password = password, + session = flowResponse.session + ) ) - ) - } - }, - passphrase = passphrase, - setupMode = SetupMode.NORMAL - )) + } + }, + passphrase = passphrase, + setupMode = SetupMode.NORMAL + ) + ) } } diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt index 2939dcf4e0..b01c1a895f 100644 --- a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import im.vector.app.core.time.DefaultClock import org.junit.rules.TestWatcher import org.junit.runner.Description import timber.log.Timber @@ -54,7 +55,7 @@ private fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) { val contentValues = ContentValues().apply { put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_TAKEN, DefaultClock().epochMillis()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { useMediaStoreScreenshotStorage( diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index cc69cfd426..d407fcb1f2 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -29,6 +29,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult @@ -60,6 +61,9 @@ class DebugMenuActivity : VectorBaseActivity() { @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject + lateinit var clock: Clock + private lateinit var buffer: ByteArray override fun initUiAndData() { @@ -165,7 +169,7 @@ class DebugMenuActivity : VectorBaseActivity() { } val builder = NotificationCompat.Builder(this, "CHAN") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title") .setContentText("Content") // No effect because it's a group summary notif @@ -180,16 +184,16 @@ class DebugMenuActivity : VectorBaseActivity() { .setName("User name") .build() ) - .addMessage("Message 1 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) - .addMessage("Message 1 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + .addMessage("Message 1 - 1", clock.epochMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 1 - 2", clock.epochMillis(), Person.Builder().setName("user 1-2").build()) val messagingStyle2 = NotificationCompat.MessagingStyle( Person.Builder() .setName("User name 2") .build() ) - .addMessage("Message 2 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) - .addMessage("Message 2 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + .addMessage("Message 2 - 1", clock.epochMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 2 - 2", clock.epochMillis(), Person.Builder().setName("user 1-2").build()) notificationManager.notify(10, builder.build()) @@ -197,7 +201,7 @@ class DebugMenuActivity : VectorBaseActivity() { 11, NotificationCompat.Builder(this, "CHAN") .setChannelId("CHAN") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title 1") .setContentText("Content 1") // For shortcut on long press on launcher icon @@ -211,7 +215,7 @@ class DebugMenuActivity : VectorBaseActivity() { notificationManager.notify( 12, NotificationCompat.Builder(this, "CHAN2") - .setWhen(System.currentTimeMillis()) + .setWhen(clock.epochMillis()) .setContentTitle("Title 2") .setContentText("Content 2") .setStyle(messagingStyle2) @@ -243,8 +247,10 @@ class DebugMenuActivity : VectorBaseActivity() { private val qrStartForActivityResult = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { - toast("QrCode: " + QrCodeScannerActivity.getResultText(activityResult.data) + - " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(activityResult.data)) + toast( + "QrCode: " + QrCodeScannerActivity.getResultText(activityResult.data) + + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(activityResult.data) + ) // Also update the current QR Code (reverse operation) // renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "") diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt index a35bb40f8f..0f00f2daa5 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt @@ -45,7 +45,8 @@ class DebugPermissionActivity : VectorBaseActivity() diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 5b3ee30722..f2904e4b1a 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -28,43 +28,45 @@ class DebugFeaturesStateFactory @Inject constructor( ) { fun create(): FeaturesState { - return FeaturesState(listOf( - createEnumFeature( - label = "Onboarding variant", - featureOverride = debugFeatures.onboardingVariant(), - featureDefault = defaultFeatures.onboardingVariant() - ), - createBooleanFeature( - label = "FTUE Splash - I already have an account", - key = DebugFeatureKeys.onboardingAlreadyHaveAnAccount, - factory = VectorFeatures::isOnboardingAlreadyHaveAccountSplashEnabled - ), - createBooleanFeature( - label = "FTUE Splash - carousel", - key = DebugFeatureKeys.onboardingSplashCarousel, - factory = VectorFeatures::isOnboardingSplashCarouselEnabled - ), - createBooleanFeature( - label = "FTUE Use Case", - key = DebugFeatureKeys.onboardingUseCase, - factory = VectorFeatures::isOnboardingUseCaseEnabled - ), - createBooleanFeature( - label = "FTUE Personalize profile", - key = DebugFeatureKeys.onboardingPersonalize, - factory = VectorFeatures::isOnboardingPersonalizeEnabled - ), - createBooleanFeature( - label = "FTUE Combined register", - key = DebugFeatureKeys.onboardingCombinedRegister, - factory = VectorFeatures::isOnboardingCombinedRegisterEnabled - ), - createBooleanFeature( - label = "Live location sharing", - key = DebugFeatureKeys.liveLocationSharing, - factory = VectorFeatures::isLiveLocationEnabled - ), - )) + return FeaturesState( + listOf( + createEnumFeature( + label = "Onboarding variant", + featureOverride = debugFeatures.onboardingVariant(), + featureDefault = defaultFeatures.onboardingVariant() + ), + createBooleanFeature( + label = "FTUE Splash - I already have an account", + key = DebugFeatureKeys.onboardingAlreadyHaveAnAccount, + factory = VectorFeatures::isOnboardingAlreadyHaveAccountSplashEnabled + ), + createBooleanFeature( + label = "FTUE Splash - carousel", + key = DebugFeatureKeys.onboardingSplashCarousel, + factory = VectorFeatures::isOnboardingSplashCarouselEnabled + ), + createBooleanFeature( + label = "FTUE Use Case", + key = DebugFeatureKeys.onboardingUseCase, + factory = VectorFeatures::isOnboardingUseCaseEnabled + ), + createBooleanFeature( + label = "FTUE Personalize profile", + key = DebugFeatureKeys.onboardingPersonalize, + factory = VectorFeatures::isOnboardingPersonalizeEnabled + ), + createBooleanFeature( + label = "FTUE Combined register", + key = DebugFeatureKeys.onboardingCombinedRegister, + factory = VectorFeatures::isOnboardingCombinedRegisterEnabled + ), + createBooleanFeature( + label = "Live location sharing", + key = DebugFeatureKeys.liveLocationSharing, + factory = VectorFeatures::isLiveLocationEnabled + ), + ) + ) } private fun createBooleanFeature(key: Preferences.Key, label: String, factory: KFunction1): Feature { diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt index 1d77d031af..dfe412117f 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt @@ -57,10 +57,12 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( debugVectorOverrides.forceHomeserverCapabilities.setOnEach { val activeDisplayNameOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeDisplayName) val activeAvatarOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeAvatar) - copy(homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy( - displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption), - avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption), - )) + copy( + homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy( + displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption), + avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption), + ) + ) } } diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt index 0fe89e7fe5..e890180c4f 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -18,13 +18,17 @@ package im.vector.app.fdroid import android.content.Context import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.time.Clock import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import timber.log.Timber object BackgroundSyncStarter { - fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { + fun start(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { if (vectorPreferences.areNotificationEnabledForDevice()) { val activeSession = activeSessionHolder.getSafeActiveSession() ?: return when (vectorPreferences.getFdroidSyncBackgroundMode()) { @@ -38,7 +42,12 @@ object BackgroundSyncStarter { } BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> { // We need to use alarm in this mode - AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay()) + AlarmSyncBroadcastReceiver.scheduleAlarm( + context, + activeSession.sessionId, + vectorPreferences.backgroundSyncDelay(), + clock + ) Timber.i("## Sync: Alarm scheduled to start syncing") } BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt index f8c30f813d..5932d2c372 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt @@ -39,24 +39,30 @@ class TestBackgroundRestrictions @Inject constructor(private val context: Fragme ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_ENABLED -> { // Background data usage is blocked for this app. Wherever possible, // the app should also use less data in the foreground. - description = stringProvider.getString(R.string.settings_troubleshoot_test_bg_restricted_failed, - "RESTRICT_BACKGROUND_STATUS_ENABLED") + description = stringProvider.getString( + R.string.settings_troubleshoot_test_bg_restricted_failed, + "RESTRICT_BACKGROUND_STATUS_ENABLED" + ) status = TestStatus.FAILED quickFix = null } ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> { // The app is whitelisted. Wherever possible, // the app should use less data in the foreground and background. - description = stringProvider.getString(R.string.settings_troubleshoot_test_bg_restricted_success, - "RESTRICT_BACKGROUND_STATUS_WHITELISTED") + description = stringProvider.getString( + R.string.settings_troubleshoot_test_bg_restricted_success, + "RESTRICT_BACKGROUND_STATUS_WHITELISTED" + ) status = TestStatus.SUCCESS quickFix = null } ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_DISABLED -> { // Data Saver is disabled. Since the device is connected to a // metered network, the app should use less data wherever possible. - description = stringProvider.getString(R.string.settings_troubleshoot_test_bg_restricted_success, - "RESTRICT_BACKGROUND_STATUS_DISABLED") + description = stringProvider.getString( + R.string.settings_troubleshoot_test_bg_restricted_success, + "RESTRICT_BACKGROUND_STATUS_DISABLED" + ) status = TestStatus.SUCCESS quickFix = null } diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt index a5154c7483..57bdf721a2 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt @@ -31,7 +31,7 @@ class TestBatteryOptimization @Inject constructor( ) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - if (isIgnoringBatteryOptimizations(context)) { + if (context.isIgnoringBatteryOptimizations()) { description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_success) status = TestStatus.SUCCESS quickFix = null diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index 4be36d7de3..09bd56654d 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -27,6 +27,7 @@ import androidx.core.content.getSystemService import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.services.VectorSyncService +import im.vector.app.core.time.Clock import org.matrix.android.sdk.api.session.sync.job.SyncService import timber.log.Timber @@ -34,10 +35,13 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Timber.d("## Sync: AlarmSyncBroadcastReceiver received intent") - val vectorPreferences = context.singletonEntryPoint() - .takeIf { it.activeSessionHolder().getSafeActiveSession() != null } - ?.vectorPreferences() - ?: return Unit.also { Timber.v("No active session, so don't launch sync service.") } + val singletonEntryPoint = context.singletonEntryPoint() + if (singletonEntryPoint.activeSessionHolder().getSafeActiveSession() == null) { + Timber.v("No active session, so don't launch sync service.") + return + } + val vectorPreferences = singletonEntryPoint.vectorPreferences() + val clock = singletonEntryPoint.clock() val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return VectorSyncService.newPeriodicIntent( @@ -52,7 +56,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { ContextCompat.startForegroundService(context, it) } catch (ex: Throwable) { Timber.i("## Sync: Failed to start service, Alarm scheduled to restart service") - scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay()) + scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay(), clock) Timber.e(ex) } } @@ -61,7 +65,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { companion object { private const val REQUEST_CODE = 0 - fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int) { + fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int, clock: Clock) { // Reschedule Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply { @@ -74,7 +78,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L + val firstMillis = clock.epochMillis() + delayInSeconds * 1000L val alarmMgr = context.getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index c1bc90c4db..aacd7723f5 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -32,7 +32,8 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { BackgroundSyncStarter.start( context, singletonEntryPoint.vectorPreferences(), - singletonEntryPoint.activeSessionHolder() + singletonEntryPoint.activeSessionHolder(), + singletonEntryPoint.clock() ) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7603e738d7..425fd1081a 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,6 +21,7 @@ import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.time.Clock import im.vector.app.fdroid.BackgroundSyncStarter import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.VectorPreferences @@ -66,7 +67,10 @@ object FcmHelper { AlarmSyncBroadcastReceiver.cancelAlarm(context) } - fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { - BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder) + fun onEnterBackground(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { + BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder, clock) } } diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index ec1b9ca7a2..2db03f3428 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -53,8 +53,10 @@ class TestTokenRegistration @Inject constructor(private val context: FragmentAct it.pushKey == fcmToken && it.state == PusherState.REGISTERED } if (pushers.isEmpty()) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_token_registration_failed, - stringProvider.getString(R.string.sas_error_unknown)) + description = stringProvider.getString( + R.string.settings_troubleshoot_test_token_registration_failed, + stringProvider.getString(R.string.sas_error_unknown) + ) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) { override fun doFix() { val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken) diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 52ad4be087..3d44f10f76 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -26,6 +26,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.time.Clock import im.vector.app.features.settings.VectorPreferences import timber.log.Timber @@ -107,7 +108,10 @@ object FcmHelper { } @Suppress("UNUSED_PARAMETER") - fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { + fun onEnterBackground(context: Context, + vectorPreferences: VectorPreferences, + activeSessionHolder: ActiveSessionHolder, + clock: Clock) { // No op } } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index e12eecfefc..0bf4eb13b6 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,6 +43,7 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration @@ -85,6 +86,7 @@ class VectorApplication : @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var clock: Clock @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider @@ -180,7 +182,7 @@ class VectorApplication : override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") - FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) + FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder, clock) } }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) @@ -211,10 +213,12 @@ class VectorApplication : private fun enableStrictModeIfNeeded() { if (BuildConfig.ENABLE_STRICT_MODE_LOGS) { - StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() - .build()) + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build() + ) } } diff --git a/vector/src/main/java/im/vector/app/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/app/core/contacts/ContactsDataSource.kt index db0fab9c42..577533f758 100644 --- a/vector/src/main/java/im/vector/app/core/contacts/ContactsDataSource.kt +++ b/vector/src/main/java/im/vector/app/core/contacts/ContactsDataSource.kt @@ -83,14 +83,16 @@ class ContactsDataSource @Inject constructor( // Get the phone numbers if (withMsisdn) { - contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, arrayOf( ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.NUMBER ), null, null, - null) + null + ) ?.use { cursor -> val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.CONTACT_ID) ?: return@use val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use @@ -122,7 +124,8 @@ class ContactsDataSource @Inject constructor( ), null, null, - null) + null + ) ?.use { cursor -> val idColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Email.CONTACT_ID) ?: return@use val emailColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Email.DATA) ?: return@use diff --git a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt index eb6f6a94f7..029fce7423 100644 --- a/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/date/VectorDateFormatter.kt @@ -22,15 +22,19 @@ import android.text.format.DateUtils import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.toTimestamp +import im.vector.app.core.time.Clock import org.threeten.bp.LocalDateTime import org.threeten.bp.Period import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject import kotlin.math.absoluteValue -class VectorDateFormatter @Inject constructor(private val context: Context, - private val localeProvider: LocaleProvider, - private val dateFormatterProviders: DateFormatterProviders) { +class VectorDateFormatter @Inject constructor( + private val context: Context, + private val localeProvider: LocaleProvider, + private val dateFormatterProviders: DateFormatterProviders, + private val clock: Clock, +) { private val hourFormatter by lazy { if (DateFormat.is24HourFormat(context)) { @@ -158,8 +162,9 @@ class VectorDateFormatter @Inject constructor(private val context: Context, private fun getRelativeDay(ts: Long): String { return DateUtils.getRelativeTimeSpanString( ts, - System.currentTimeMillis(), + clock.epochMillis(), DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_SHOW_WEEKDAY).toString() + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() } } diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index 283437c679..dc88229a10 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,6 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer @@ -46,6 +47,8 @@ interface SingletonEntryPoint { fun navigator(): Navigator + fun clock(): Clock + fun errorFormatter(): ErrorFormatter fun bugReporter(): BugReporter diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index 0db7e4e8ea..ae6cb39bd1 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -50,6 +50,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob @@ -173,7 +174,7 @@ object VectorStaticModule { return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) @Provides @NamedGlobalScope fun providesGlobalScope(): CoroutineScope { diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt index 8f70808087..a9e223709a 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -27,6 +27,7 @@ import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper.Listener import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog @@ -45,7 +46,8 @@ import java.io.File class GalleryOrCameraDialogHelper( // must implement GalleryOrCameraDialogHelper.Listener private val fragment: Fragment, - private val colorProvider: ColorProvider + private val colorProvider: ColorProvider, + private val clock: Clock, ) { interface Listener { fun onImageReady(uri: Uri?) @@ -91,7 +93,7 @@ class GalleryOrCameraDialogHelper( } private fun startUCrop(image: MultiPickerImageType) { - val destinationFile = File(activity.cacheDir, image.displayName.insertBeforeLast("_e_${System.currentTimeMillis()}")) + val destinationFile = File(activity.cacheDir, image.displayName.insertBeforeLast("_e_${clock.epochMillis()}")) val uri = image.contentUri createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title)) .withAspectRatio(1f, 1f) @@ -107,10 +109,12 @@ class GalleryOrCameraDialogHelper( fun show() { MaterialAlertDialogBuilder(activity) .setTitle(R.string.attachment_type_dialog_title) - .setItems(arrayOf( - fragment.getString(R.string.attachment_type_camera), - fragment.getString(R.string.attachment_type_gallery) - )) { _, which -> + .setItems( + arrayOf( + fragment.getString(R.string.attachment_type_camera), + fragment.getString(R.string.attachment_type_gallery) + ) + ) { _, which -> onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery) } .setPositiveButton(R.string.action_cancel, null) diff --git a/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt index 415c82b330..b594dbcac4 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt @@ -113,13 +113,17 @@ class UnrecognizedCertificateDialog @Inject constructor( views.sslFingerprintTitle.text = stringProvider.getString(R.string.ssl_fingerprint_hash, unrecognizedFingerprint.hashType.toString()) views.sslFingerprint.text = unrecognizedFingerprint.displayableHexRepr if (userId != null) { - views.sslUserId.text = stringProvider.getString(R.string.generic_label_and_value, + views.sslUserId.text = stringProvider.getString( + R.string.generic_label_and_value, stringProvider.getString(R.string.username), - userId) + userId + ) } else { - views.sslUserId.text = stringProvider.getString(R.string.generic_label_and_value, + views.sslUserId.text = stringProvider.getString( + R.string.generic_label_and_value, stringProvider.getString(R.string.hs_url), - homeServerUrl) + homeServerUrl + ) } if (existing) { if (homeServerConnectionConfigHasFingerprints) { diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 2eb36d758e..a6f80a256e 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -155,15 +155,17 @@ class DefaultErrorFormatter @Inject constructor( } private fun identityServerError(identityServiceError: IdentityServiceError): String { - return stringProvider.getString(when (identityServiceError) { - IdentityServiceError.OutdatedIdentityServer -> R.string.identity_server_error_outdated_identity_server - IdentityServiceError.OutdatedHomeServer -> R.string.identity_server_error_outdated_home_server - IdentityServiceError.NoIdentityServerConfigured -> R.string.identity_server_error_no_identity_server_configured - IdentityServiceError.TermsNotSignedException -> R.string.identity_server_error_terms_not_signed - IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported - IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error - IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error - IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided - }) + return stringProvider.getString( + when (identityServiceError) { + IdentityServiceError.OutdatedIdentityServer -> R.string.identity_server_error_outdated_identity_server + IdentityServiceError.OutdatedHomeServer -> R.string.identity_server_error_outdated_home_server + IdentityServiceError.NoIdentityServerConfigured -> R.string.identity_server_error_no_identity_server_configured + IdentityServiceError.TermsNotSignedException -> R.string.identity_server_error_terms_not_signed + IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported + IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error + IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error + IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided + } + ) } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index f8b8b83178..a3ef1cf4e3 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -30,6 +30,11 @@ inline fun T.ooi(block: (T) -> Unit): T = also(block) */ fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() +/** + * Return empty CharSequence if the CharSequence is null + */ +fun CharSequence?.orEmpty() = this ?: "" + /** * Check if a CharSequence is a phone number */ diff --git a/vector/src/main/java/im/vector/app/core/glide/ElementToDecryptOption.kt b/vector/src/main/java/im/vector/app/core/glide/ElementToDecryptOption.kt index f4f854406e..04b351515a 100644 --- a/vector/src/main/java/im/vector/app/core/glide/ElementToDecryptOption.kt +++ b/vector/src/main/java/im/vector/app/core/glide/ElementToDecryptOption.kt @@ -22,4 +22,5 @@ import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt const val ElementToDecryptOptionKey = "im.vector.app.core.glide.ElementToDecrypt" val ELEMENT_TO_DECRYPT = Option.memory( - ElementToDecryptOptionKey, ElementToDecrypt("", "", "")) + ElementToDecryptOptionKey, ElementToDecrypt("", "", "") +) diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index c53db12b6b..a643ee908c 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -121,7 +121,8 @@ class VectorGlideDataFetcher(context: Context, fileName = data.filename, mimeType = data.mimeType, url = data.url, - elementToDecrypt = data.elementToDecrypt) + elementToDecrypt = data.elementToDecrypt + ) } withContext(Dispatchers.Main) { result.fold( diff --git a/vector/src/main/java/im/vector/app/core/linkify/VectorAutoLinkPatterns.kt b/vector/src/main/java/im/vector/app/core/linkify/VectorAutoLinkPatterns.kt index bc45ab5676..b9fab37e43 100644 --- a/vector/src/main/java/im/vector/app/core/linkify/VectorAutoLinkPatterns.kt +++ b/vector/src/main/java/im/vector/app/core/linkify/VectorAutoLinkPatterns.kt @@ -24,14 +24,16 @@ object VectorAutoLinkPatterns { private const val LAT_OR_LONG_OR_ALT_NUMBER = "-?\\d+(?:\\.\\d+)?" private const val COORDINATE_SYSTEM = ";crs=[\\w-]+" - val GEO_URI: Regex = Regex("(?:geo:)?" + - "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + - "," + - "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + - "(?:" + "," + LAT_OR_LONG_OR_ALT_NUMBER + ")?" + // altitude - "(?:" + COORDINATE_SYSTEM + ")?" + - "(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" + // uncertainty in meters - "(?:" + - ";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + // dafuk - ")*", RegexOption.IGNORE_CASE) + val GEO_URI: Regex = Regex( + "(?:geo:)?" + + "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + + "," + + "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + + "(?:" + "," + LAT_OR_LONG_OR_ALT_NUMBER + ")?" + // altitude + "(?:" + COORDINATE_SYSTEM + ")?" + + "(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" + // uncertainty in meters + "(?:" + + ";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + // dafuk + ")*", RegexOption.IGNORE_CASE + ) } diff --git a/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt b/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt index 7bedeaa4ff..128591fd96 100755 --- a/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt @@ -62,7 +62,8 @@ class ButtonStateView @JvmOverloads constructor(context: Context, attrs: Attribu context.theme.obtainStyledAttributes( attrs, R.styleable.ButtonStateView, - 0, 0) + 0, 0 + ) .apply { try { if (getBoolean(R.styleable.ButtonStateView_bsv_use_flat_button, true)) { diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index febcfc5ef2..fcfd38bccc 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -260,27 +260,29 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private fun handleGlobalError(globalError: GlobalError) { when (globalError) { - is GlobalError.InvalidToken -> - handleInvalidToken(globalError) - is GlobalError.ConsentNotGivenError -> - consentNotGivenHelper.displayDialog(globalError.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") - is GlobalError.CertificateError -> - handleCertificateError(globalError) + is GlobalError.InvalidToken -> handleInvalidToken(globalError) + is GlobalError.ConsentNotGivenError -> displayConsentNotGivenDialog(globalError) + is GlobalError.CertificateError -> handleCertificateError(globalError) GlobalError.ExpiredAccount -> Unit // TODO Handle account expiration is GlobalError.InitialSyncRequest -> handleInitialSyncRequest(globalError) } } + private fun displayConsentNotGivenDialog(globalError: GlobalError.ConsentNotGivenError) { + consentNotGivenHelper.displayDialog(globalError.consentUri, activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") + } + private fun handleInitialSyncRequest(initialSyncRequest: GlobalError.InitialSyncRequest) { MaterialAlertDialogBuilder(this) .setTitle(R.string.initial_sync_request_title) .setMessage( - getString(R.string.initial_sync_request_content, getString( + getString( + R.string.initial_sync_request_content, getString( when (initialSyncRequest.reason) { InitialSyncRequestReason.IGNORED_USERS_LIST_CHANGE -> R.string.initial_sync_request_reason_unignored_users } - )) + ) + ) ) .setPositiveButton(R.string.ok) { _, _ -> MainActivity.restartApp(this, MainActivityArgs(clearCache = true)) @@ -318,7 +320,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver mainActivityStarted = true - MainActivity.restartApp(this, + MainActivity.restartApp( + this, MainActivityArgs( clearCredentials = !globalError.softLogout, isUserLoggedOut = true, diff --git a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt index 78266cf5ee..34def7e060 100644 --- a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt @@ -56,11 +56,13 @@ class PushRulePreference : VectorPreference { * Refresh the summary */ private fun refreshSummary() { - summary = context.getString(when (index) { - NotificationIndex.OFF -> R.string.notification_off - NotificationIndex.SILENT -> R.string.notification_silent - NotificationIndex.NOISY, null -> R.string.notification_noisy - }) + summary = context.getString( + when (index) { + NotificationIndex.OFF -> R.string.notification_off + NotificationIndex.SILENT -> R.string.notification_silent + NotificationIndex.NOISY, null -> R.string.notification_noisy + } + ) } override fun onBindViewHolder(holder: PreferenceViewHolder) { diff --git a/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt b/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt index 22fc1758f1..2dd6f058d7 100755 --- a/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt @@ -33,6 +33,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import im.vector.app.R import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber /** @@ -152,16 +153,12 @@ open class VectorPreference : Preference { */ private fun addClickListeners(view: View) { view.setOnLongClickListener { - if (null != onPreferenceLongClickListener) { - onPreferenceLongClickListener!!.onPreferenceLongClick(this@VectorPreference) - } else false + onPreferenceLongClickListener?.onPreferenceLongClick(this@VectorPreference).orFalse() } view.setOnClickListener { // call only the click listener - if (onPreferenceClickListener != null) { - onPreferenceClickListener.onPreferenceClick(this@VectorPreference) - } + onPreferenceClickListener?.onPreferenceClick(this@VectorPreference) } } } diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index b2d9382aae..702092679f 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt @@ -140,13 +140,15 @@ class CallRingPlayerOutgoing( mediaPlayer.setOnErrorListener(MediaPlayerErrorListener()) mediaPlayer.isLooping = true if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { - mediaPlayer.setAudioAttributes(AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - // TODO Change to ? - // .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - // .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build()) + mediaPlayer.setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + // TODO Change to ? + // .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + // .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + ) } else { @Suppress("DEPRECATION") mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING) diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt index 5dbea8dcc4..131276bea6 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt @@ -33,6 +33,8 @@ import androidx.work.WorkerParameters import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.PendingIntentCompat +import im.vector.app.core.time.Clock +import im.vector.app.core.time.DefaultClock import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode import org.matrix.android.sdk.api.Matrix @@ -77,6 +79,7 @@ class VectorSyncService : SyncService() { @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var matrix: Matrix + @Inject lateinit var clock: Clock override fun provideMatrix() = matrix @@ -102,7 +105,8 @@ class VectorSyncService : SyncService() { syncTimeoutSeconds = syncTimeoutSeconds, syncDelaySeconds = syncDelaySeconds, isPeriodic = true, - isNetworkBack = false + isNetworkBack = false, + currentTimeMillis = clock.epochMillis() ) } @@ -114,9 +118,10 @@ class VectorSyncService : SyncService() { val rescheduleSyncWorkRequest: WorkRequest = OneTimeWorkRequestBuilder() .setInputData(RestartWhenNetworkOn.createInputData(sessionId, syncTimeoutSeconds, syncDelaySeconds, isPeriodic)) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() ) .build() @@ -137,20 +142,27 @@ class VectorSyncService : SyncService() { } // I do not move or rename this class, since I'm not sure about the side effect regarding the WorkManager - class RestartWhenNetworkOn(appContext: Context, workerParams: WorkerParameters) : - Worker(appContext, workerParams) { + class RestartWhenNetworkOn( + appContext: Context, + workerParams: WorkerParameters + ) : Worker(appContext, workerParams) { + override fun doWork(): Result { Timber.d("## Sync: RestartWhenNetworkOn.doWork()") val sessionId = inputData.getString(KEY_SESSION_ID) ?: return Result.failure() val syncTimeoutSeconds = inputData.getInt(KEY_SYNC_TIMEOUT_SECONDS, BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS) val syncDelaySeconds = inputData.getInt(KEY_SYNC_DELAY_SECONDS, BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS) val isPeriodic = inputData.getBoolean(KEY_IS_PERIODIC, false) + + // Not sure how to inject a Clock here + val clock = DefaultClock() applicationContext.rescheduleSyncService( sessionId = sessionId, syncTimeoutSeconds = syncTimeoutSeconds, syncDelaySeconds = syncDelaySeconds, isPeriodic = isPeriodic, - isNetworkBack = true + isNetworkBack = true, + currentTimeMillis = clock.epochMillis() ) // Indicate whether the work finished successfully with the Result return Result.success() @@ -182,7 +194,8 @@ private fun Context.rescheduleSyncService(sessionId: String, syncTimeoutSeconds: Int, syncDelaySeconds: Int, isPeriodic: Boolean, - isNetworkBack: Boolean) { + isNetworkBack: Boolean, + currentTimeMillis: Long) { Timber.d("## Sync: rescheduleSyncService") val intent = if (isPeriodic) { VectorSyncService.newPeriodicIntent( @@ -208,7 +221,7 @@ private fun Context.rescheduleSyncService(sessionId: String, } else { PendingIntent.getService(this, 0, intent, PendingIntentCompat.FLAG_IMMUTABLE) } - val firstMillis = System.currentTimeMillis() + syncDelaySeconds * 1000L + val firstMillis = currentTimeMillis + syncDelaySeconds * 1000L val alarmMgr = getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt index 3a79e7e328..8477eddeea 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt @@ -107,11 +107,13 @@ class ReadReceiptsView @JvmOverloads constructor( else -> if (displayNames.size >= 2) { val qty = readReceipts.size - 2 - context.resources.getQuantityString(R.plurals.two_and_some_others_read, + context.resources.getQuantityString( + R.plurals.two_and_some_others_read, qty, displayNames[0], displayNames[1], - qty) + qty + ) } else { context.resources.getQuantityString(R.plurals.fallback_users_read, readReceipts.size) } diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index a9375b6545..d961dcaa46 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -147,8 +147,11 @@ fun openFileSelection(activity: Activity, * Send an email to address with optional subject and message */ fun sendMailTo(address: String, subject: String? = null, message: String? = null, activity: Activity) { - val intent = Intent(Intent.ACTION_SENDTO, Uri.fromParts( - "mailto", address, null)) + val intent = Intent( + Intent.ACTION_SENDTO, Uri.fromParts( + "mailto", address, null + ) + ) intent.putExtra(Intent.EXTRA_SUBJECT, subject) intent.putExtra(Intent.EXTRA_TEXT, message) @@ -248,7 +251,12 @@ private fun appendTimeToFilename(name: String): String { return """${filename}_$dateExtension.$fileExtension""" } -suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { +suspend fun saveMedia(context: Context, + file: File, + title: String, + mediaMimeType: String?, + notificationUtils: NotificationUtils, + currentTimeMillis: Long) { withContext(Dispatchers.IO) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val filename = appendTimeToFilename(title) @@ -257,8 +265,8 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType put(MediaStore.Images.Media.TITLE, filename) put(MediaStore.Images.Media.DISPLAY_NAME, filename) put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_ADDED, currentTimeMillis) + put(MediaStore.Images.Media.DATE_TAKEN, currentTimeMillis) } val externalContentUri = when { mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI @@ -289,7 +297,7 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType } } } else { - saveMediaLegacy(context, mediaMimeType, title, file) + saveMediaLegacy(context, mediaMimeType, title, file, currentTimeMillis) } } } @@ -298,7 +306,8 @@ suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, - file: File) { + file: File, + currentTimeMillis: Long) { val state = Environment.getExternalStorageState() if (Environment.MEDIA_MOUNTED != state) { context.toast(context.getString(R.string.error_saving_media_file)) @@ -319,7 +328,7 @@ private fun saveMediaLegacy(context: Context, } else { title } - val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) + val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename, currentTimeMillis) if (savedFile != null) { val downloadManager = context.getSystemService() downloadManager?.addCompletedDownload( @@ -329,7 +338,8 @@ private fun saveMediaLegacy(context: Context, mediaMimeType ?: MimeTypes.OctetStream, savedFile.absolutePath, savedFile.length(), - true) + true + ) addToGallery(savedFile, mediaMimeType, context) } } catch (error: Throwable) { @@ -408,10 +418,11 @@ fun selectTxtFileToWrite( * @param sourceFile the file source path * @param dstDirPath the dst path * @param outputFilename optional the output filename + * @param currentTimeMillis the current time in milliseconds * @return the created file */ @Suppress("DEPRECATION") -fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? { +fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?, currentTimeMillis: Long): File? { // defines another name for the external media var dstFileName: String @@ -423,7 +434,7 @@ fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: Strin if (dotPos > 0) { fileExt = sourceFile.name.substring(dotPos) } - dstFileName = "vector_" + System.currentTimeMillis() + fileExt + dstFileName = "vector_$currentTimeMillis$fileExt" } else { dstFileName = outputFilename } diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 1fa2b8151a..f8ff12ddb2 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -43,21 +43,20 @@ import im.vector.app.features.notifications.NotificationUtils * This user option appears on Android M but Android O enforces its usage and kills apps not * authorised by the user to run in background. * - * @param context the context * @return true if battery optimisations are ignored */ -fun isIgnoringBatteryOptimizations(context: Context): Boolean { +fun Context.isIgnoringBatteryOptimizations(): Boolean { // no issue before Android M, battery optimisations did not exist return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - context.getSystemService()?.isIgnoringBatteryOptimizations(context.packageName) == true + getSystemService()?.isIgnoringBatteryOptimizations(packageName) == true } -fun isAirplaneModeOn(context: Context): Boolean { - return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +fun Context.isAirplaneModeOn(): Boolean { + return Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 } -fun isAnimationDisabled(context: Context): Boolean { - return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f +fun Context.isAnimationEnabled(): Boolean { + return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f } /** diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 9d54475e8c..42693a53f9 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -44,5 +44,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false override fun isLiveLocationEnabled(): Boolean = false - override fun isScreenSharingEnabled(): Boolean = false + override fun isScreenSharingEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 98440632d8..6734744c29 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -140,22 +140,26 @@ class AttachmentTypeSelectorView(context: Context, private fun animateWindowInCircular(anchor: View, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) - val animator = ViewAnimationUtils.createCircularReveal(contentView, + val animator = ViewAnimationUtils.createCircularReveal( + contentView, coordinates.first, coordinates.second, 0f, - max(contentView.width, contentView.height).toFloat()) + max(contentView.width, contentView.height).toFloat() + ) animator.duration = ANIMATION_DURATION.toLong() animator.start() } private fun animateWindowOutCircular(anchor: View, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) - val animator = ViewAnimationUtils.createCircularReveal(getContentView(), + val animator = ViewAnimationUtils.createCircularReveal( + getContentView(), coordinates.first, coordinates.second, max(getContentView().width, getContentView().height).toFloat(), - 0f) + 0f + ) animator.duration = ANIMATION_DURATION.toLong() animator.addListener(object : AnimatorListenerAdapter() { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 5b9c3f7fb4..729ac10d4b 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.OnSnapPositionChangeListener import im.vector.app.core.utils.SnapOnScrollListener import im.vector.app.core.utils.attachSnapHelperWithListener @@ -64,7 +65,8 @@ data class AttachmentsPreviewArgs( class AttachmentsPreviewFragment @Inject constructor( private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController, - private val colorProvider: ColorProvider + private val colorProvider: ColorProvider, + private val clock: Clock, ) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback { private val fragmentArgs: AttachmentsPreviewArgs by args() @@ -192,7 +194,7 @@ class AttachmentsPreviewFragment @Inject constructor( private fun handleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState - val destinationFile = File(requireContext().cacheDir, currentAttachment.name.insertBeforeLast("_edited_image_${System.currentTimeMillis()}")) + val destinationFile = File(requireContext().cacheDir, currentAttachment.name.insertBeforeLast("_edited_image_${clock.epochMillis()}")) val uri = currentAttachment.queryUri createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), currentAttachment.name) .getIntent(requireContext()) diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index b3fc36e5bc..f0158fc4d6 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -89,6 +89,8 @@ class CallControlsView @JvmOverloads constructor( views.videoToggleIcon.setImageResource(R.drawable.ic_video_off) views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera) } + views.videoToggleIcon.isEnabled = !state.isSharingScreen + views.videoToggleIcon.alpha = if (state.isSharingScreen) 0.5f else 1f when (callState) { is CallState.LocalRinging -> { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index ea9adcde85..a904658e9c 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -24,6 +24,7 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.res.Configuration import android.graphics.Color +import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import android.os.Build import android.os.Bundle @@ -32,6 +33,7 @@ import android.util.Rational import android.view.MenuItem import android.view.View import android.view.WindowManager +import androidx.activity.result.ActivityResult import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -76,6 +78,7 @@ import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.webrtc.EglBase import org.webrtc.RendererCommon +import org.webrtc.ScreenCapturerAndroid import timber.log.Timber import javax.inject.Inject @@ -161,6 +164,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } } + + // Bind to service in case of user killed the app while there is an ongoing call + bindToScreenCaptureService() } override fun onNewIntent(intent: Intent?) { @@ -636,18 +642,40 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private val screenSharingPermissionActivityResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { - callViewModel.handle(VectorCallViewActions.StartScreenSharing) - // We need to start a foreground service with a sticky notification during screen sharing if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ContextCompat.startForegroundService( - this, - Intent(this, ScreenCaptureService::class.java) - ) - screenCaptureServiceConnection.bind() + // We need to start a foreground service with a sticky notification during screen sharing + startScreenSharingService(activityResult) + } else { + startScreenSharing(activityResult) } } } + private fun startScreenSharing(activityResult: ActivityResult) { + val videoCapturer = ScreenCapturerAndroid(activityResult.data, object : MediaProjection.Callback() { + override fun onStop() { + Timber.i("User revoked the screen capturing permission") + } + }) + callViewModel.handle(VectorCallViewActions.StartScreenSharing(videoCapturer)) + } + + private fun startScreenSharingService(activityResult: ActivityResult) { + ContextCompat.startForegroundService( + this, + Intent(this, ScreenCaptureService::class.java) + ) + bindToScreenCaptureService(activityResult) + } + + private fun bindToScreenCaptureService(activityResult: ActivityResult? = null) { + screenCaptureServiceConnection.bind(object : ScreenCaptureServiceConnection.Callback { + override fun onServiceConnected() { + activityResult?.let { startScreenSharing(it) } + } + }) + } + private fun handleShowScreenSharingPermissionDialog() { getSystemService()?.let { navigator.openScreenSharingPermissionDialog(it.createScreenCaptureIntent(), screenSharingPermissionActivityResultLauncher) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index c84f733b9a..cec118f296 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.transfer.CallTransferResult +import org.webrtc.VideoCapturer sealed class VectorCallViewActions : VectorViewModelAction { object EndCall : VectorCallViewActions() @@ -41,5 +42,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions() object TransferCall : VectorCallViewActions() object ToggleScreenSharing : VectorCallViewActions() - object StartScreenSharing : VectorCallViewActions() + data class StartScreenSharing(val videoCapturer: VideoCapturer) : VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 55a0219bfe..e2cedbe1b0 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -145,9 +145,10 @@ class VectorCallViewModel @AssistedInject constructor( override fun onCallEnded(callId: String) { withState { state -> if (state.otherKnownCallInfo?.callId == callId) { - setState { copy(otherKnownCallInfo = null) } + setState { copy(otherKnownCallInfo = null, isSharingScreen = false) } } } + _viewEvents.post(VectorCallViewEvents.StopScreenSharingService) } override fun onCurrentCallChange(call: WebRtcCall?) { @@ -156,9 +157,10 @@ class VectorCallViewModel @AssistedInject constructor( } } - override fun onAudioDevicesChange() { - val currentSoundDevice = callManager.audioManager.selectedDevice ?: return - if (currentSoundDevice == CallAudioManager.Device.Phone) { + override fun onAudioDevicesChange() = withState { state -> + val currentSoundDevice = callManager.audioManager.selectedDevice ?: return@withState + val webRtcCall = callManager.getCallById(state.callId) + if (webRtcCall != null && shouldActivateProximitySensor(webRtcCall)) { proximityManager.start() } else { proximityManager.stop() @@ -205,7 +207,7 @@ class VectorCallViewModel @AssistedInject constructor( callManager.addListener(callManagerListener) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice - if (currentSoundDevice == CallAudioManager.Device.Phone) { + if (shouldActivateProximitySensor(webRtcCall)) { proximityManager.start() } setState { @@ -224,13 +226,18 @@ class VectorCallViewModel @AssistedInject constructor( formattedDuration = webRtcCall.formattedDuration(), isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(), - transferee = computeTransfereeState(webRtcCall.mxCall) + transferee = computeTransfereeState(webRtcCall.mxCall), + isSharingScreen = webRtcCall.isSharingScreen() ) } updateOtherKnownCall(webRtcCall) } } + private fun shouldActivateProximitySensor(webRtcCall: WebRtcCall): Boolean { + return callManager.audioManager.selectedDevice == CallAudioManager.Device.Phone && !webRtcCall.isSharingScreen() + } + private fun WebRtcCall.extractCallInfo(): VectorCallViewState.CallInfo { val assertedIdentity = this.remoteAssertedIdentity val matrixItem = if (assertedIdentity != null) { @@ -349,7 +356,8 @@ class VectorCallViewModel @AssistedInject constructor( handleToggleScreenSharing(state.isSharingScreen) } is VectorCallViewActions.StartScreenSharing -> { - call?.startSharingScreen() + call?.startSharingScreen(action.videoCapturer) + proximityManager.stop() setState { copy(isSharingScreen = true) } @@ -366,6 +374,9 @@ class VectorCallViewModel @AssistedInject constructor( _viewEvents.post( VectorCallViewEvents.StopScreenSharingService ) + if (callManager.audioManager.selectedDevice == CallAudioManager.Device.Phone) { + proximityManager.start() + } } else { _viewEvents.post( VectorCallViewEvents.ShowScreenSharingPermissionDialog diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index 07062fc732..b0da4f0e18 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String import im.vector.app.features.call.conference.jwt.JitsiJWTFactory @@ -46,7 +47,9 @@ class JitsiService @Inject constructor( private val rawService: RawService, private val stringProvider: StringProvider, private val themeProvider: ThemeProvider, - private val jitsiJWTFactory: JitsiJWTFactory) { + private val jitsiJWTFactory: JitsiJWTFactory, + private val clock: Clock, +) { companion object { const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" @@ -60,7 +63,7 @@ class JitsiService @Inject constructor( suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { // Build data for a jitsi widget - val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() + val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + clock.epochMillis() val preferredJitsiDomain = tryOrNull { rawService.getElementWellknown(session.sessionParams) ?.jitsiServer diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt index f1a4975751..489b2d1eae 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt @@ -21,6 +21,7 @@ import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.services.VectorService +import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils import javax.inject.Inject @@ -28,6 +29,7 @@ import javax.inject.Inject class ScreenCaptureService : VectorService() { @Inject lateinit var notificationUtils: NotificationUtils + @Inject lateinit var clock: Clock private val binder = LocalBinder() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -37,7 +39,7 @@ class ScreenCaptureService : VectorService() { } private fun showStickyNotification() { - val notificationId = System.currentTimeMillis().toInt() + val notificationId = clock.epochMillis().toInt() val notification = notificationUtils.buildScreenSharingNotification() startForeground(notificationId, notification) } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt index 922e9676a8..aa7c7f450a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt @@ -27,11 +27,20 @@ class ScreenCaptureServiceConnection @Inject constructor( private val context: Context ) : ServiceConnection { + interface Callback { + fun onServiceConnected() + } + private var isBound = false private var screenCaptureService: ScreenCaptureService? = null + private var callback: Callback? = null - fun bind() { - if (!isBound) { + fun bind(callback: Callback) { + this.callback = callback + + if (isBound) { + callback.onServiceConnected() + } else { Intent(context, ScreenCaptureService::class.java).also { intent -> context.bindService(intent, this, 0) } @@ -45,10 +54,12 @@ class ScreenCaptureServiceConnection @Inject constructor( override fun onServiceConnected(className: ComponentName, binder: IBinder) { screenCaptureService = (binder as ScreenCaptureService.LocalBinder).getService() isBound = true + callback?.onServiceConnected() } override fun onServiceDisconnected(className: ComponentName) { isBound = false screenCaptureService = null + callback = null } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index fed61c3b15..5a100edcf2 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -80,10 +80,12 @@ import org.webrtc.MediaConstraints import org.webrtc.MediaStream import org.webrtc.PeerConnection import org.webrtc.PeerConnectionFactory +import org.webrtc.RtpSender import org.webrtc.RtpTransceiver import org.webrtc.SessionDescription import org.webrtc.SurfaceTextureHelper import org.webrtc.SurfaceViewRenderer +import org.webrtc.VideoCapturer import org.webrtc.VideoSource import org.webrtc.VideoTrack import timber.log.Timber @@ -95,6 +97,7 @@ import kotlin.coroutines.CoroutineContext private const val STREAM_ID = "userMedia" private const val AUDIO_TRACK_ID = "${STREAM_ID}a0" private const val VIDEO_TRACK_ID = "${STREAM_ID}v0" +private const val SCREEN_TRACK_ID = "${STREAM_ID}s0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() private const val INVITE_TIMEOUT_IN_MS = 60_000L @@ -153,13 +156,16 @@ class WebRtcCall( private var makingOffer: Boolean = false private var ignoreOffer: Boolean = false - private var videoCapturer: CameraVideoCapturer? = null + private var videoCapturer: VideoCapturer? = null private val availableCamera = ArrayList() private var cameraInUse: CameraProxy? = null private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null + private var videoSender: RtpSender? = null + private var screenSender: RtpSender? = null + private val timer = CountUpTimer(1000L).apply { tickListener = object : CountUpTimer.TickListener { override fun onTick(milliseconds: Long) { @@ -617,7 +623,7 @@ class WebRtcCall( val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource) Timber.tag(loggerTag.value).v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}") videoTrack.setEnabled(true) - peerConnection?.addTrack(videoTrack, listOf(STREAM_ID)) + videoSender = peerConnection?.addTrack(videoTrack, listOf(STREAM_ID)) localVideoSource = videoSource localVideoTrack = videoTrack } @@ -670,15 +676,11 @@ class WebRtcCall( isRemoteOnHold = true isLocalOnHold = true direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY - timer.pause() } else { isRemoteOnHold = false isLocalOnHold = wasLocalOnHold onCallBecomeActive(this@WebRtcCall) direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV - if (!isLocalOnHold) { - timer.resume() - } } for (transceiver in peerConnection?.transceivers ?: emptyList()) { transceiver.direction = direction @@ -722,7 +724,7 @@ class WebRtcCall( Timber.tag(loggerTag.value).v("switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return@launch - videoCapturer?.switchCamera( + (videoCapturer as? CameraVideoCapturer)?.switchCamera( object : CameraVideoCapturer.CameraSwitchHandler { // Invoked on success. |isFrontCamera| is true if the new camera is front facing. override fun onCameraSwitchDone(isFrontCamera: Boolean) { @@ -770,12 +772,60 @@ class WebRtcCall( return currentCaptureFormat } - fun startSharingScreen() { - // TODO. Will be handled within the next PR. + fun startSharingScreen(videoCapturer: VideoCapturer) { + val factory = peerConnectionFactoryProvider.get() ?: return + + this.videoCapturer = videoCapturer + + val localMediaStream = factory.createLocalMediaStream(STREAM_ID) + val videoSource = factory.createVideoSource(videoCapturer.isScreencast) + + startCapturingScreen(videoCapturer, videoSource) + + removeLocalSurfaceRenderers() + + showScreenLocally(factory, videoSource, localMediaStream) + + videoSender?.let { removeStream(it) } + + screenSender = peerConnection?.addTrack(localVideoTrack, listOf(STREAM_ID)) } fun stopSharingScreen() { - // TODO. Will be handled within the next PR. + localVideoTrack?.setEnabled(false) + screenSender?.let { removeStream(it) } + if (mxCall.isVideoCall) { + peerConnectionFactoryProvider.get()?.let { configureVideoTrack(it) } + } + updateMuteStatus() + sessionScope?.launch(dispatcher) { attachViewRenderersInternal() } + } + + private fun removeStream(sender: RtpSender) { + peerConnection?.removeTrack(sender) + } + + private fun showScreenLocally(factory: PeerConnectionFactory, videoSource: VideoSource?, localMediaStream: MediaStream?) { + localVideoTrack = factory.createVideoTrack(SCREEN_TRACK_ID, videoSource).apply { setEnabled(true) } + localMediaStream?.addTrack(localVideoTrack) + localSurfaceRenderers.forEach { it.get()?.let { localVideoTrack?.addSink(it) } } + } + + private fun removeLocalSurfaceRenderers() { + localSurfaceRenderers.forEach { it.get()?.let { localVideoTrack?.removeSink(it) } } + } + + private fun startCapturingScreen(videoCapturer: VideoCapturer, videoSource: VideoSource) { + val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext) + videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) + videoCapturer.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) + } + + /** + * Returns true if the user is sharing the screen, false otherwise. + */ + fun isSharingScreen(): Boolean { + return localVideoTrack?.enabled().orFalse() && localVideoTrack?.id() == SCREEN_TRACK_ID } private suspend fun release() { @@ -941,11 +991,6 @@ class WebRtcCall( wasLocalOnHold = nowOnHold if (prevOnHold != nowOnHold) { isLocalOnHold = nowOnHold - if (nowOnHold) { - timer.pause() - } else { - timer.resume() - } listeners.forEach { tryOrNull { it.onHoldUnhold() } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index fe12bf1ec7..10e822c947 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -215,9 +215,10 @@ class WebRtcCallManager @Inject constructor( } Timber.tag(loggerTag.value).v("PeerConnectionFactory.initialize") - PeerConnectionFactory.initialize(PeerConnectionFactory - .InitializationOptions.builder(context.applicationContext) - .createInitializationOptions() + PeerConnectionFactory.initialize( + PeerConnectionFactory + .InitializationOptions.builder(context.applicationContext) + .createInitializationOptions() ) val options = PeerConnectionFactory.Options() @@ -226,7 +227,8 @@ class WebRtcCallManager @Inject constructor( /* enableIntelVp8Encoder */ true, /* enableH264HighProfile */ - true) + true + ) val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext) Timber.tag(loggerTag.value).v("PeerConnectionFactory.createPeerConnectionFactory ...") peerConnectionFactory = PeerConnectionFactory.builder() @@ -304,7 +306,8 @@ class WebRtcCallManager @Inject constructor( } CallService.onOutgoingCallRinging( context = context.applicationContext, - callId = mxCall.callId) + callId = mxCall.callId + ) // start the activity now context.startActivity(VectorCallActivity.newIntent(context, webRtcCall, VectorCallActivity.OUTGOING_CREATED)) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt index 6cfe0bf686..a8c3f41efe 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -83,29 +83,45 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( val progressObserver = object : StepProgressListener { override fun onStepProgress(step: StepProgressListener.Step) { when (step) { - is StepProgressListener.Step.ComputingKey -> { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + - "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), - step.progress, - step.total)) + is StepProgressListener.Step.ComputingKey -> { + loadingEvent.postValue( + WaitingViewData( + stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + step.progress, + step.total + ) + ) } is StepProgressListener.Step.DownloadingKey -> { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + - "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), - isIndeterminate = true)) + loadingEvent.postValue( + WaitingViewData( + stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), + isIndeterminate = true + ) + ) } - is StepProgressListener.Step.ImportingKey -> { + is StepProgressListener.Step.ImportingKey -> { Timber.d("backupKeys.ImportingKey.progress: ${step.progress}") // Progress 0 can take a while, display an indeterminate progress in this case if (step.progress == 0) { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + - "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - isIndeterminate = true)) + loadingEvent.postValue( + WaitingViewData( + stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + isIndeterminate = true + ) + ) } else { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + - "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - step.progress, - step.total)) + loadingEvent.postValue( + WaitingViewData( + stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + step.progress, + step.total + ) + ) } } } @@ -205,7 +221,8 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( try { val result = awaitCallback { - keysBackup.restoreKeyBackupWithPassword(keyVersion, + keysBackup.restoreKeyBackupWithPassword( + keyVersion, passphrase, null, session.myUserId, @@ -230,7 +247,8 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( try { val result = awaitCallback { - keysBackup.restoreKeysWithRecoveryKey(keyVersion, + keysBackup.restoreKeysWithRecoveryKey( + keyVersion, recoveryKey, null, session.myUserId, diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt index c4663fd3bc..d26c1e2134 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt @@ -40,10 +40,14 @@ class KeysBackupRestoreSuccessFragment @Inject constructor() : VectorBaseFragmen if (compareValues(sharedViewModel.importKeyResult?.totalNumberOfKeys, 0) > 0) { sharedViewModel.importKeyResult?.let { - val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1, - it.totalNumberOfKeys, it.totalNumberOfKeys) - val part2 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part2, - it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys) + val part1 = resources.getQuantityString( + R.plurals.keys_backup_restore_success_description_part1, + it.totalNumberOfKeys, it.totalNumberOfKeys + ) + val part2 = resources.getQuantityString( + R.plurals.keys_backup_restore_success_description_part2, + it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys + ) views.successDetailsText.text = String.format("%s\n%s", part1, part2) } // We don't put emoji in string xml as it will crash on old devices diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 9bf8050939..d281360678 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -132,9 +132,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor( if (data.keysBackupVersionTrust()?.usable == false) { description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence()) } else { - description(host.stringProvider - .getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup) + .toEpoxyCharSequence() + ) } } @@ -200,27 +202,35 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor( val deviceId: String = it.deviceId ?: "" if (!isDeviceKnown) { - description(host.stringProvider - .getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId) + .toEpoxyCharSequence() + ) endIconResourceId(R.drawable.e2e_warning) } else { if (isSignatureValid) { if (host.session.sessionParams.deviceId == it.deviceId) { - description(host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_this_device) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_this_device) + .toEpoxyCharSequence() + ) endIconResourceId(R.drawable.e2e_verified) } else { if (isDeviceVerified) { - description(host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId) + .toEpoxyCharSequence() + ) endIconResourceId(R.drawable.e2e_verified) } else { - description(host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId) + .toEpoxyCharSequence() + ) endIconResourceId(R.drawable.e2e_warning) } } @@ -228,13 +238,17 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor( // Invalid signature endIconResourceId(R.drawable.e2e_warning) if (isDeviceVerified) { - description(host.stringProvider - .getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId) + .toEpoxyCharSequence() + ) } else { - description(host.stringProvider - .getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId) - .toEpoxyCharSequence()) + description( + host.stringProvider + .getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId) + .toEpoxyCharSequence() + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt index 34aa7ba0ee..dfa7d1aaa3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import com.nulabinc.zxcvbn.Strength import im.vector.app.R import im.vector.app.core.platform.WaitingViewData +import im.vector.app.core.time.Clock import im.vector.app.core.utils.LiveEvent import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener @@ -37,7 +38,9 @@ import javax.inject.Inject /** * The shared view model between all fragments. */ -class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { +class KeysBackupSetupSharedViewModel @Inject constructor( + private val clock: Clock, +) : ViewModel() { companion object { const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2" @@ -85,7 +88,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { fun prepareRecoveryKey(context: Context, withPassphrase: String?) { // Update requestId - currentRequestId.value = System.currentTimeMillis() + currentRequestId.value = clock.epochMillis() isCreatingBackupVersion.value = true recoveryKey.value = null @@ -101,9 +104,11 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() { return } - loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_step3_generating_key_status), + loadingStatus.value = WaitingViewData( + context.getString(R.string.keys_backup_setup_step3_generating_key_status), progress, - total) + total + ) } }, object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index e5d7ade3ce..61148f3aab 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -146,7 +146,8 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment { - ReAuthActivity.newIntent(requireContext(), + ReAuthActivity.newIntent( + requireContext(), event.flowResponse, event.lastErrorCode, - getString(R.string.initialize_cross_signing)).let { intent -> + getString(R.string.initialize_cross_signing) + ).let { intent -> reAuthActivityResultLauncher.launch(intent) } } @@ -201,9 +203,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment, argsParcelable: Parcelable? = null) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { - replace(R.id.bottomSheetFragmentContainer, + replace( + R.id.bottomSheetFragmentContainer, fragmentClass.java, argsParcelable?.toMvRxBundle(), fragmentClass.simpleName diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 753e9f1942..c26aec45dd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -122,7 +122,8 @@ class BootstrapCrossSigningTask @Inject constructor( params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), - isIndeterminate = true) + isIndeterminate = true + ) ) Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") @@ -151,7 +152,8 @@ class BootstrapCrossSigningTask @Inject constructor( params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), - isIndeterminate = true) + isIndeterminate = true + ) ) Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index a85c318a29..b67970e61f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -404,7 +404,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( } viewModelScope.launch(Dispatchers.IO) { - bootstrapTask.invoke(this, + bootstrapTask.invoke( + this, Params( userInteractiveAuthInterceptor = interceptor, progressListener = progressListener, diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index a57159075f..e0aa4592a4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -18,6 +18,7 @@ package im.vector.app.features.crypto.verification import android.content.Context import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer @@ -43,7 +44,9 @@ import javax.inject.Singleton class IncomingVerificationRequestHandler @Inject constructor( private val context: Context, private var avatarRenderer: Provider, - private val popupAlertManager: PopupAlertManager) : VerificationService.Listener { + private val popupAlertManager: PopupAlertManager, + private val clock: Clock, +) : VerificationService.Listener { private var session: Session? = null @@ -105,7 +108,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } ) // 10mn expiration - expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L) + expirationTimestamp = clock.epochMillis() + (10 * 60 * 1000L) } popupAlertManager.postVectorAlert(alert) } @@ -167,14 +170,15 @@ class IncomingVerificationRequestHandler @Inject constructor( } } dismissedAction = Runnable { - session?.cryptoService()?.verificationService()?.declineVerificationRequestInDMs(pr.otherUserId, + session?.cryptoService()?.verificationService()?.declineVerificationRequestInDMs( + pr.otherUserId, pr.transactionId ?: "", pr.roomId ?: "" ) } colorAttribute = R.attr.vctr_notice_secondary // 5mn expiration - expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L) + expirationTimestamp = clock.epochMillis() + (5 * 60 * 1000L) } popupAlertManager.postVectorAlert(alert) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/SupportedVerificationMethodsProvider.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/SupportedVerificationMethodsProvider.kt index 00ba676926..6bbd37a3a8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/SupportedVerificationMethodsProvider.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/SupportedVerificationMethodsProvider.kt @@ -33,7 +33,8 @@ class SupportedVerificationMethodsProvider @Inject constructor( // Element supports SAS verification VerificationMethod.SAS, // Element is able to show QR codes - VerificationMethod.QR_CODE_SHOW) + VerificationMethod.QR_CODE_SHOW + ) .apply { if (hardwareInfo.hasBackCamera()) { // Element is able to scan QR codes, and a Camera is available diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 18a1363d71..9c6c22b6ca 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -95,12 +95,14 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment dismiss() is VerificationBottomSheetViewEvents.AccessSecretStore -> { - secretStartForActivityResult.launch(SharedSecureStorageActivity.newIntent( - requireContext(), - null, // use default key - listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), - SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS - )) + secretStartForActivityResult.launch( + SharedSecureStorageActivity.newIntent( + requireContext(), + null, // use default key + listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), + SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS + ) + ) } is VerificationBottomSheetViewEvents.ModalError -> { MaterialAlertDialogBuilder(requireContext()) @@ -183,7 +185,8 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment, argsParcelable: Parcelable? = null) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { - replace(R.id.bottomSheetFragmentContainer, + replace( + R.id.bottomSheetFragmentContainer, fragmentClass.java, argsParcelable?.toMvRxBundle(), fragmentClass.simpleName @@ -360,31 +364,37 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment?) { + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources + // Just check that the key is valid and store it, the backup will be used megolm session per + // megolm session when an UISI is encountered + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { @@ -432,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( session.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() ?: return@launch - awaitCallback { - session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - version, - computeRecoveryKey(secret.fromBase64()), - null, - null, - null, - it - ) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + val isValid = awaitCallback { + session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it) + } + if (isValid) { + session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version) } - awaitCallback { session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index 31a7956db3..cf6bcc58c0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -74,9 +74,12 @@ class VerificationChooseMethodFragment @Inject constructor( } override fun doVerifyBySas() = withState(sharedViewModel) { state -> - sharedViewModel.handle(VerificationAction.StartSASVerification( - state.otherUserMxItem?.id ?: "", - state.pendingRequest.invoke()?.transactionId ?: "")) + sharedViewModel.handle( + VerificationAction.StartSASVerification( + state.otherUserMxItem?.id ?: "", + state.pendingRequest.invoke()?.transactionId ?: "" + ) + ) } private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> @@ -115,10 +118,12 @@ class VerificationChooseMethodFragment @Inject constructor( } private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) { state -> - sharedViewModel.handle(VerificationAction.RemoteQrCodeScanned( - state.otherUserMxItem?.id ?: "", - state.pendingRequest.invoke()?.transactionId ?: "", - remoteQrCode - )) + sharedViewModel.handle( + VerificationAction.RemoteQrCodeScanned( + state.otherUserMxItem?.id ?: "", + state.pendingRequest.invoke()?.transactionId ?: "", + remoteQrCode + ) + ) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index 7696bb8f5b..a1f902f8f4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -102,7 +102,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( // Get the QR code now, because transaction is already created, so transactionCreated() will not be called val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "") - return VerificationChooseMethodViewState(otherUserId = args.otherUserId, + return VerificationChooseMethodViewState( + otherUserId = args.otherUserId, isMe = session.myUserId == pvr?.otherUserId, canCrossSign = session.cryptoService().crossSigningService().canCrossSign(), transactionId = args.verificationId ?: "", diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt index ee77444b2e..7f6678a73c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -52,10 +52,13 @@ class VerificationConclusionController @Inject constructor( ConclusionState.SUCCESS -> { bottomSheetVerificationNoticeItem { id("notice") - notice(host.stringProvider.getString( - if (state.isSelfVerification) R.string.verification_conclusion_ok_self_notice - else R.string.verification_conclusion_ok_notice) - .toEpoxyCharSequence()) + notice( + host.stringProvider.getString( + if (state.isSelfVerification) R.string.verification_conclusion_ok_self_notice + else R.string.verification_conclusion_ok_notice + ) + .toEpoxyCharSequence() + ) } bottomSheetVerificationBigImageItem { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt index 4b59e2e6fb..95dd4263f9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt @@ -60,9 +60,13 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor( init { withState { state -> - refreshStateFromTx(session.cryptoService().verificationService() - .getExistingTransaction(state.otherUser?.id ?: "", state.transactionId - ?: "") as? SasVerificationTransaction) + refreshStateFromTx( + session.cryptoService().verificationService() + .getExistingTransaction( + state.otherUser?.id ?: "", state.transactionId + ?: "" + ) as? SasVerificationTransaction + ) } session.cryptoService().verificationService().addListener(this) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 2de03f296e..285d0f728f 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -117,7 +117,8 @@ class DiscoverySettingsFragment @Inject constructor( termsActivityResultLauncher, TermsService.ServiceType.IdentityService, state.identityServer()?.serverUrl?.ensureProtocol() ?: "", - null) + null + ) } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt index ca43e80ea6..fe2be713af 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt @@ -67,8 +67,10 @@ class SetIdentityServerFragment @Inject constructor( state.defaultIdentityServerUrl.toReducedUrl() ) .toSpannable() - .colorizeMatchingText(state.defaultIdentityServerUrl.toReducedUrl(), - colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + .colorizeMatchingText( + state.defaultIdentityServerUrl.toReducedUrl(), + colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary) + ) views.identityServerSetDefaultNotice.isVisible = true views.identityServerSetDefaultSubmit.isVisible = true @@ -129,7 +131,8 @@ class SetIdentityServerFragment @Inject constructor( termsActivityResultLauncher, TermsService.ServiceType.IdentityService, it.identityServerUrl, - null) + null + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 326b350f30..fd2862f5f0 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -66,9 +66,11 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @UiThread fun render(matrixItem: MatrixItem, imageView: ImageView) { - render(GlideApp.with(imageView), + render( + GlideApp.with(imageView), matrixItem, - DrawableImageViewTarget(imageView)) + DrawableImageViewTarget(imageView) + ) } // fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) { @@ -97,9 +99,11 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @UiThread fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { - render(glideRequests, + render( + glideRequests, matrixItem, - DrawableImageViewTarget(imageView)) + DrawableImageViewTarget(imageView) + ) } @UiThread @@ -200,12 +204,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active it.load(resolvedUrl) } else { val avatarColor = matrixItemColorProvider.getColor(matrixItem) - it.load(TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) - .toBitmap(width = iconSize, height = iconSize)) + it.load( + TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) + .toBitmap(width = iconSize, height = iconSize) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 12ae67fe6e..cc202868cc 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -142,9 +142,11 @@ class HomeActivity : } // Here we want to change current space to the newly created one, and then immediately open the default room if (spaceId != null) { - navigator.switchToSpace(context = this, + navigator.switchToSpace( + context = this, spaceId = spaceId, - postSwitchOption) + postSwitchOption + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index f0e27e2ee7..05973de49d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -226,8 +226,8 @@ class HomeActivityViewModel @AssistedInject constructor( val knownRooms = activeSessionHolder.getSafeActiveSession() ?.roomService() ?.getRoomSummaries(roomSummaryQueryParams { - memberships = Membership.activeMemberships() - })?.size ?: 0 + memberships = Membership.activeMemberships() + })?.size ?: 0 // Prompt once to the user if (knownRooms > 1 && !vectorPreferences.didAskUserToEnableSessionPush()) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index f3973e5d4c..2753ba817d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -444,7 +444,8 @@ class HomeDetailFragment @Inject constructor( it.syncState, it.incrementalSyncStatus, it.pushCounter, - vectorPreferences.developerShowDebugInfo()) + vectorPreferences.developerShowDebugInfo() + ) hasUnreadRooms = it.hasUnreadMessages } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 3ca19b39f9..34bdc5fcd3 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.time.Clock import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -56,10 +57,12 @@ data class DeviceDetectionInfo( val currentSessionTrust: Boolean ) -class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, - session: Session, - private val vectorPreferences: VectorPreferences) : - VectorViewModel(initialState) { +class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( + @Assisted initialState: UnknownDevicesState, + session: Session, + private val vectorPreferences: VectorPreferences, + clock: Clock, +) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() @@ -75,11 +78,10 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted private val ignoredDeviceList = ArrayList() init { - val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) .firstOrNull { it.deviceId == session.sessionParams.deviceId } ?.firstTimeSeenLocalTs - ?: System.currentTimeMillis() + ?: clock.epochMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") ignoredDeviceList.addAll( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index f95baae36b..fa19e39ae7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail +import im.vector.app.core.time.Clock import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -45,7 +46,9 @@ fun ChatEffect.toMessageType(): String { * precisely an event decrypted with a few delay won't trigger an effect; it's acceptable) * Events that are more that 10s old won't trigger effects */ -class ChatEffectManager @Inject constructor() { +class ChatEffectManager @Inject constructor( + private val clock: Clock, +) { interface Delegate { fun stopEffects() @@ -61,7 +64,7 @@ class ChatEffectManager @Inject constructor() { fun checkForEffect(event: TimelineEvent) { val age = event.root.ageLocalTs ?: 0 - val now = System.currentTimeMillis() + val now = clock.epochMillis() // messages older than 10s should not trigger any effect if ((now - age) >= 10_000) return val content = event.root.getClearContent()?.toModel() ?: return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt index 9f10805f95..1f1124b8c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -33,14 +33,18 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyViewHolder import im.vector.app.R +import im.vector.app.core.time.Clock import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import kotlin.math.abs import kotlin.math.min -class RoomMessageTouchHelperCallback(private val context: Context, - @DrawableRes actionIcon: Int, - private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() { +class RoomMessageTouchHelperCallback( + private val context: Context, + @DrawableRes actionIcon: Int, + private val handler: QuickReplayHandler, + private val clock: Clock, +) : EpoxyTouchHelperCallback() { interface QuickReplayHandler { fun performQuickReplyOnHolder(model: EpoxyModel<*>) @@ -141,7 +145,7 @@ class RoomMessageTouchHelperCallback(private val context: Context, private fun drawReplyButton(canvas: Canvas, itemView: View) { // Timber.v("drawReplyButton") val translationX = abs(itemView.translationX) - val newTime = System.currentTimeMillis() + val newTime = clock.epochMillis() val dt = min(17, newTime - lastReplyButtonAnimationTime) lastReplyButtonAnimationTime = newTime val showing = translationX >= minShowDistance diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index 193dc42f33..d75b9ff69d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -63,12 +63,14 @@ class StartCallActionsHandler( if (currentCall?.signalingRoomId == roomId) { onTapToReturnToCall() } else if (!state.isAllowedToStartWebRTCCall) { - showDialogWithMessage(fragment.getString( - if (state.isDm()) { - R.string.no_permissions_to_start_webrtc_call_in_direct_room - } else { - R.string.no_permissions_to_start_webrtc_call - }) + showDialogWithMessage( + fragment.getString( + if (state.isDm()) { + R.string.no_permissions_to_start_webrtc_call_in_direct_room + } else { + R.string.no_permissions_to_start_webrtc_call + } + ) ) } else { safeStartCall(isVideoCall) @@ -79,13 +81,15 @@ class StartCallActionsHandler( // can you add widgets?? if (!state.isAllowedToManageWidgets) { // You do not have permission to start a conference call in this room - showDialogWithMessage(fragment.getString( - if (state.isDm()) { - R.string.no_permissions_to_start_conf_call_in_direct_room - } else { - R.string.no_permissions_to_start_conf_call - } - )) + showDialogWithMessage( + fragment.getString( + if (state.isDm()) { + R.string.no_permissions_to_start_conf_call_in_direct_room + } else { + R.string.no_permissions_to_start_conf_call + } + ) + ) } else { if (state.hasActiveJitsiWidget()) { // A conference is already in progress, return @@ -123,18 +127,22 @@ class StartCallActionsHandler( val startCallAction = RoomDetailAction.StartCall(isVideoCall) timelineViewModel.pendingAction = startCallAction if (isVideoCall) { - if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, + if (checkPermissions( + PERMISSIONS_FOR_VIDEO_IP_CALL, fragment.requireActivity(), startCallActivityResultLauncher, - R.string.permissions_rationale_msg_camera_and_audio)) { + R.string.permissions_rationale_msg_camera_and_audio + )) { timelineViewModel.pendingAction = null timelineViewModel.handle(startCallAction) } } else { - if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, + if (checkPermissions( + PERMISSIONS_FOR_AUDIO_IP_CALL, fragment.requireActivity(), startCallActivityResultLauncher, - R.string.permissions_rationale_msg_record_audio)) { + R.string.permissions_rationale_msg_record_audio + )) { timelineViewModel.pendingAction = null timelineViewModel.handle(startCallAction) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 00857ccffe..9d36fdc580 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -105,6 +105,7 @@ import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createUIHandler +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedSnackbar @@ -296,7 +297,7 @@ class TimelineFragment @Inject constructor( private const val ircPattern = " (IRC)" } - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val timelineArgs: TimelineArgs by args() private val glideRequests by lazy { @@ -586,6 +587,10 @@ class TimelineFragment @Inject constructor( } private fun handleChatEffect(chatEffect: ChatEffect) { + if (!requireContext().isAnimationEnabled()) { + Timber.d("Do not perform chat effect, animations are disabled.") + return + } when (chatEffect) { ChatEffect.CONFETTI -> { views.viewKonfetti.isVisible = true @@ -666,11 +671,13 @@ class TimelineFragment @Inject constructor( ).apply { directListener = { granted -> if (granted) { - timelineViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed( - widget = it.widget, - userJustAccepted = true, - grantedEvents = it.grantedEvents - )) + timelineViewModel.handle( + RoomDetailAction.EnsureNativeWidgetAllowed( + widget = it.widget, + userJustAccepted = true, + grantedEvents = it.grantedEvents + ) + ) } } } @@ -791,25 +798,29 @@ class TimelineFragment @Inject constructor( override fun onSendVoiceMessage() { messageComposerViewModel.handle( - MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId())) + MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId()) + ) updateRecordingUiState(RecordingUiState.Idle) } override fun onDeleteVoiceMessage() { messageComposerViewModel.handle( - MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId())) + MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()) + ) updateRecordingUiState(RecordingUiState.Idle) } override fun onRecordingLimitReached() { messageComposerViewModel.handle( - MessageComposerAction.PauseRecordingVoiceMessage) + MessageComposerAction.PauseRecordingVoiceMessage + ) updateRecordingUiState(RecordingUiState.Draft) } override fun onRecordingWaveformClicked() { messageComposerViewModel.handle( - MessageComposerAction.PauseRecordingVoiceMessage) + MessageComposerAction.PauseRecordingVoiceMessage + ) updateRecordingUiState(RecordingUiState.Draft) } @@ -827,7 +838,8 @@ class TimelineFragment @Inject constructor( private fun updateRecordingUiState(state: RecordingUiState) { messageComposerViewModel.handle( - MessageComposerAction.OnVoiceRecordingUiStateChanged(state)) + MessageComposerAction.OnVoiceRecordingUiStateChanged(state) + ) } } } @@ -1443,7 +1455,7 @@ class TimelineFragment @Inject constructor( } } } - val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler) + val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler, clock) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(views.timelineRecyclerView) } @@ -1527,9 +1539,11 @@ class TimelineFragment @Inject constructor( attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) attachmentTypeSelector.setAttachmentVisibility( AttachmentTypeSelectorView.Type.LOCATION, - vectorPreferences.isLocationSharingEnabled()) + vectorPreferences.isLocationSharingEnabled() + ) attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()) + AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() + ) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -2186,7 +2200,8 @@ class TimelineFragment @Inject constructor( file = it, title = action.messageContent.body, mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) } .onFailure { @@ -2291,12 +2306,18 @@ class TimelineFragment @Inject constructor( handleCancelSend(action) } is EventSharedAction.ReportContentSpam -> { - timelineViewModel.handle(RoomDetailAction.ReportContent( - action.eventId, action.senderId, "This message is spam", spam = true)) + timelineViewModel.handle( + RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is spam", spam = true + ) + ) } is EventSharedAction.ReportContentInappropriate -> { - timelineViewModel.handle(RoomDetailAction.ReportContent( - action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) + timelineViewModel.handle( + RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is inappropriate", inappropriate = true + ) + ) } is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) @@ -2480,7 +2501,8 @@ class TimelineFragment @Inject constructor( roomId = timelineArgs.roomId, displayName = timelineViewModel.getRoomSummary()?.displayName, roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl) + avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + ) navigator.openThreadList(it, roomThreadDetailArgs) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 7b46eed4f8..fc31c72df3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -218,7 +218,8 @@ class TimelineViewModel @AssistedInject constructor( .flattenParentIds.firstOrNull { it.isNotBlank() }, // force persist, because if not on resume the AppStateHandler will resume // the current space from what was persisted on enter background - persistNow = true) + persistNow = true + ) } } } @@ -373,9 +374,12 @@ class TimelineViewModel @AssistedInject constructor( threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE }?.let { true } ?: false val numberOfLocalUnreadThreads = threadList?.size ?: 0 - copy(threadNotificationBadgeState = ThreadNotificationBadgeState( - numberOfLocalUnreadThreads = numberOfLocalUnreadThreads, - isUserMentioned = isUserMentioned)) + copy( + threadNotificationBadgeState = ThreadNotificationBadgeState( + numberOfLocalUnreadThreads = numberOfLocalUnreadThreads, + isUserMentioned = isUserMentioned + ) + ) } } @@ -515,10 +519,13 @@ class TimelineViewModel @AssistedInject constructor( private fun handleSendSticker(action: RoomDetailAction.SendSticker) { val content = initialState.rootThreadEventId?.let { - action.stickerContent.copy(relatesTo = RelationDefaultContent( - type = RelationType.THREAD, - isFallingBack = true, - eventId = it)) + action.stickerContent.copy( + relatesTo = RelationDefaultContent( + type = RelationType.THREAD, + isFallingBack = true, + eventId = it + ) + ) } ?: action.stickerContent room.sendService().sendEvent(EventType.STICKER, content.toContent()) @@ -846,10 +853,12 @@ class TimelineViewModel @AssistedInject constructor( mxcUrl.startsWith("content://") if (isLocalSendingFile) { tryOrNull { Uri.parse(mxcUrl) }?.let { - _viewEvents.post(RoomDetailViewEvents.OpenFile( - it, - action.messageFileContent.mimeType - )) + _viewEvents.post( + RoomDetailViewEvents.OpenFile( + it, + action.messageFileContent.mimeType + ) + ) } } else { viewModelScope.launch { @@ -861,21 +870,25 @@ class TimelineViewModel @AssistedInject constructor( session.fileService().downloadFile(messageContent = action.messageFileContent) } - _viewEvents.post(RoomDetailViewEvents.DownloadFileState( - action.messageFileContent.mimeType, - result.getOrNull(), - result.exceptionOrNull() - )) + _viewEvents.post( + RoomDetailViewEvents.DownloadFileState( + action.messageFileContent.mimeType, + result.getOrNull(), + result.exceptionOrNull() + ) + ) canOpen = result.isSuccess } if (canOpen) { // We can now open the file session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri -> - _viewEvents.post(RoomDetailViewEvents.OpenFile( - uri, - action.messageFileContent.mimeType - )) + _viewEvents.post( + RoomDetailViewEvents.OpenFile( + uri, + action.messageFileContent.mimeType + ) + ) } } } @@ -1024,7 +1037,8 @@ class TimelineViewModel @AssistedInject constructor( supportedVerificationMethodsProvider.provide(), action.otherUserId, room.roomId, - action.transactionId)) { + action.transactionId + )) { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) } else { // TODO @@ -1035,7 +1049,8 @@ class TimelineViewModel @AssistedInject constructor( session.cryptoService().verificationService().declineVerificationRequestInDMs( action.otherUserId, action.transactionId, - room.roomId) + room.roomId + ) } private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { @@ -1048,9 +1063,13 @@ class TimelineViewModel @AssistedInject constructor( session.cryptoService().verificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let { if (it.handledByOtherSession) return if (!it.isFinished) { - _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action.copy( - otherUserId = it.otherUserId - ))) + _viewEvents.post( + RoomDetailViewEvents.ActionSuccess( + action.copy( + otherUserId = it.otherUserId + ) + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 8351af14dc..58ec9c76bc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -203,14 +203,16 @@ class MessageComposerViewModel @AssistedInject constructor( is SendMode.Regular -> { when (val parsedCommand = commandParser.parseSlashCommand( textMessage = action.text, - isInThreadTimeline = state.isInThreadTimeline())) { + isInThreadTimeline = state.isInThreadTimeline() + )) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room if (state.rootThreadEventId != null) { room.relationService().replyInThread( rootThreadEventId = state.rootThreadEventId, replyInThreadText = action.text, - autoMarkdown = action.autoMarkdown) + autoMarkdown = action.autoMarkdown + ) } else { room.sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) } @@ -236,7 +238,8 @@ class MessageComposerViewModel @AssistedInject constructor( room.relationService().replyInThread( rootThreadEventId = state.rootThreadEventId, replyInThreadText = parsedCommand.message, - autoMarkdown = false) + autoMarkdown = false + ) } else { room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) } @@ -292,12 +295,14 @@ class MessageComposerViewModel @AssistedInject constructor( rootThreadEventId = state.rootThreadEventId, replyInThreadText = parsedCommand.message, msgType = MessageType.MSGTYPE_EMOTE, - autoMarkdown = action.autoMarkdown) + autoMarkdown = action.autoMarkdown + ) } else { room.sendService().sendTextMessage( text = parsedCommand.message, msgType = MessageType.MSGTYPE_EMOTE, - autoMarkdown = action.autoMarkdown) + autoMarkdown = action.autoMarkdown + ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() @@ -308,7 +313,8 @@ class MessageComposerViewModel @AssistedInject constructor( room.relationService().replyInThread( rootThreadEventId = state.rootThreadEventId, replyInThreadText = parsedCommand.message, - formattedText = rainbowGenerator.generate(message)) + formattedText = rainbowGenerator.generate(message) + ) } else { room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) } @@ -322,7 +328,8 @@ class MessageComposerViewModel @AssistedInject constructor( rootThreadEventId = state.rootThreadEventId, replyInThreadText = parsedCommand.message, msgType = MessageType.MSGTYPE_EMOTE, - formattedText = rainbowGenerator.generate(message)) + formattedText = rainbowGenerator.generate(message) + ) } else { room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) } @@ -337,11 +344,13 @@ class MessageComposerViewModel @AssistedInject constructor( room.relationService().replyInThread( rootThreadEventId = state.rootThreadEventId, replyInThreadText = text, - formattedText = formattedText) + formattedText = formattedText + ) } else { room.sendService().sendFormattedTextMessage( text, - formattedText) + formattedText + ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() @@ -500,10 +509,12 @@ class MessageComposerViewModel @AssistedInject constructor( val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { - room.relationService().editTextMessage(state.sendMode.timelineEvent, + room.relationService().editTextMessage( + state.sendMode.timelineEvent, messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, action.text, - action.autoMarkdown) + action.autoMarkdown + ) } else { Timber.w("Same message content, do not send edition") } @@ -516,7 +527,8 @@ class MessageComposerViewModel @AssistedInject constructor( quotedEvent = state.sendMode.timelineEvent, text = action.text.toString(), autoMarkdown = action.autoMarkdown, - rootThreadEventId = state.rootThreadEventId) + rootThreadEventId = state.rootThreadEventId + ) _viewEvents.post(MessageComposerViewEvents.MessageSent) popDraft() } @@ -530,7 +542,8 @@ class MessageComposerViewModel @AssistedInject constructor( rootThreadEventId = it, replyInThreadText = action.text.toString(), autoMarkdown = action.autoMarkdown, - eventReplied = timelineEvent) + eventReplied = timelineEvent + ) } ?: room.relationService().replyToMessage( eventReplied = timelineEvent, replyText = action.text.toString(), @@ -604,10 +617,12 @@ class MessageComposerViewModel @AssistedInject constructor( private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { - val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { - ChatEffect.CONFETTI -> R.string.default_message_emote_confetti - ChatEffect.SNOWFALL -> R.string.default_message_emote_snow - }) + val defaultMessage = stringProvider.getString( + when (sendChatEffect.chatEffect) { + ChatEffect.CONFETTI -> R.string.default_message_emote_confetti + ChatEffect.SNOWFALL -> R.string.default_message_emote_snow + } + ) room.sendService().sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) } else { room.sendService().sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) @@ -847,7 +862,8 @@ class MessageComposerViewModel @AssistedInject constructor( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), compressBeforeSending = false, roomIds = emptySet(), - rootThreadEventId = rootThreadEventId) + rootThreadEventId = rootThreadEventId + ) } else { audioMessageHelper.deleteRecording() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 95553eb1cd..016a39d919 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import kotlin.random.Random /** * Describes the current send mode: @@ -35,7 +36,7 @@ sealed interface SendMode { val text: String, val fromSharing: Boolean, // This is necessary for forcing refresh on selectSubscribe - private val ts: Long = System.currentTimeMillis() + private val random: Int = Random.nextInt() ) : SendMode data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode @@ -83,7 +84,8 @@ data class MessageComposerViewState( constructor(args: TimelineArgs) : this( roomId = args.roomId, startsThread = args.threadTimelineArgs?.startsThread.orFalse(), - rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId + ) fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 0db5e7f177..81e4d8fd5f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -30,6 +30,7 @@ import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter @@ -47,7 +48,8 @@ class SearchResultController @Inject constructor( private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, private val displayableEventFormatter: DisplayableEventFormatter, - private val userPreferencesProvider: UserPreferencesProvider + private val userPreferencesProvider: UserPreferencesProvider, + private val clock: Clock, ) : TypedEpoxyController() { var listener: Listener? = null @@ -110,7 +112,7 @@ class SearchResultController @Inject constructor( val spannable = setHighLightedText(text, data.highlights) ?: return@forEach val eventDate = Calendar.getInstance().apply { - timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis() + timeInMillis = eventAndSender.event.originServerTs ?: clock.epochMillis() } if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) { GenericHeaderItem_() @@ -125,8 +127,9 @@ class SearchResultController @Inject constructor( .avatarRenderer(avatarRenderer) .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)) .spannable(spannable.toEpoxyCharSequence()) - .sender(eventAndSender.sender - ?: eventAndSender.event.senderId?.let { session.roomService().getRoomMember(it, data.roomId) }?.toMatrixItem() + .sender( + eventAndSender.sender + ?: eventAndSender.event.senderId?.let { session.roomService().getRoomMember(it, data.roomId) }?.toMatrixItem() ) .threadDetails(event.threadDetails) .threadSummaryFormatted(displayableEventFormatter.formatThreadSummary(event.threadDetails?.threadSummaryLatestEvent).toString()) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 8cea57399a..6c9f7ac4ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,6 +31,7 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull +import im.vector.app.core.time.Clock import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -78,19 +79,21 @@ import javax.inject.Inject import kotlin.math.min import kotlin.system.measureTimeMillis -class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, - private val vectorPreferences: VectorPreferences, - private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, - private val timelineItemFactory: TimelineItemFactory, - private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val mergedHeaderItemFactory: MergedHeaderItemFactory, - private val session: Session, - @TimelineEventControllerHandler - private val backgroundHandler: Handler, - private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, - private val readReceiptsItemFactory: ReadReceiptsItemFactory, - private val reactionListFactory: ReactionsSummaryFactory +class TimelineEventController @Inject constructor( + private val dateFormatter: VectorDateFormatter, + private val vectorPreferences: VectorPreferences, + private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, + private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, + private val timelineItemFactory: TimelineItemFactory, + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val mergedHeaderItemFactory: MergedHeaderItemFactory, + private val session: Session, + @TimelineEventControllerHandler + private val backgroundHandler: Handler, + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, + private val readReceiptsItemFactory: ReadReceiptsItemFactory, + private val reactionListFactory: ReactionsSummaryFactory, + private val clock: Clock, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { /** @@ -209,8 +212,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onChanged(position: Int, count: Int, payload: Any?) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onChanged(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) (position until position + count).forEach { // Invalidate cache modelCache[it] = null @@ -238,8 +243,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onMoved(fromPosition: Int, toPosition: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) val model = modelCache.removeAt(fromPosition) modelCache.add(toPosition, model) requestModelBuild() @@ -249,8 +256,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onInserted(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onInserted(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) repeat(count) { modelCache.add(position, null) } @@ -261,8 +270,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onRemoved(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() - Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " + - "\ncurrentSnapshot has size of ${currentSnapshot.size} items") + Timber.v( + "listUpdateCallback.onRemoved(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items" + ) repeat(count) { modelCache.removeAt(position) } @@ -314,7 +325,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (partialState.roomSummary?.membership != Membership.JOIN) { return } - val timestamp = System.currentTimeMillis() + val timestamp = clock.epochMillis() val showingForwardLoader = LoadingItem_() .id("forward_loading_item_$timestamp") @@ -406,14 +417,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEvent = it, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId) + rootThreadEventId = partialState.rootThreadEventId + ) } val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { timelineEventVisibilityHelper.shouldShowEvent( timelineEvent = it, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId) + rootThreadEventId = partialState.rootThreadEventId + ) } val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( @@ -466,7 +479,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec position: Int, receiptsByEvents: Map>): CacheItemData { val wantsDateSeparator = wantsDateSeparator(event, nextEvent) - val mergedHeaderModel = mergedHeaderItemFactory.create(event, + val mergedHeaderModel = mergedHeaderItemFactory.create( + event, nextEvent = nextEvent, partialState = partialState, items = this@TimelineEventController.currentSnapshot, @@ -537,7 +551,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEvent = event, highlightedEventId = partialState.highlightedEventId, isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId)) { + rootThreadEventId = partialState.rootThreadEventId + )) { lastShownEventId = event.eventId } if (lastShownEventId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index 0cf7e60eae..57b2912aff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -57,7 +57,8 @@ data class MessageActionState( roomId = args.roomId, eventId = args.eventId, informationData = args.informationData, - isFromThreadTimeline = args.isFromThreadTimeline) + isFromThreadTimeline = args.isFromThreadTimeline + ) fun senderName(): String = informationData.memberName?.toString() ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index df2a1fbe81..2f9f2331e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -377,19 +377,23 @@ class MessageActionsViewModel @AssistedInject constructor( if (canRedact(timelineEvent, actionPermissions)) { if (timelineEvent.root.getClearType() in EventType.POLL_START) { - add(EventSharedAction.Redact( - eventId, - askForReason = informationData.senderId != session.myUserId, - dialogTitleRes = R.string.delete_poll_dialog_title, - dialogDescriptionRes = R.string.delete_poll_dialog_content - )) + add( + EventSharedAction.Redact( + eventId, + askForReason = informationData.senderId != session.myUserId, + dialogTitleRes = R.string.delete_poll_dialog_title, + dialogDescriptionRes = R.string.delete_poll_dialog_content + ) + ) } else { - add(EventSharedAction.Redact( - eventId, - askForReason = informationData.senderId != session.myUserId, - dialogTitleRes = R.string.delete_event_dialog_title, - dialogDescriptionRes = R.string.delete_event_dialog_content - )) + add( + EventSharedAction.Redact( + eventId, + askForReason = informationData.senderId != session.myUserId, + dialogTitleRes = R.string.delete_event_dialog_title, + dialogDescriptionRes = R.string.delete_event_dialog_content + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 321745355e..0548a6ad18 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -51,7 +51,8 @@ class ViewEditHistoryBottomSheet : views.bottomSheetRecyclerView.configureWith( epoxyController, dividerDrawable = R.drawable.divider_horizontal_on_secondary, - hasFixedSize = false) + hasFixedSize = false + ) views.bottomSheetTitle.text = context?.getString(R.string.message_edits) } @@ -68,11 +69,13 @@ class ViewEditHistoryBottomSheet : companion object { fun newInstance(roomId: String, informationData: MessageInformationData): ViewEditHistoryBottomSheet { return ViewEditHistoryBottomSheet().apply { - setArguments(TimelineEventFragmentArgs( - eventId = informationData.eventId, - roomId = roomId, - informationData = informationData - )) + setArguments( + TimelineEventFragmentArgs( + eventId = informationData.eventId, + roomId = roomId, + informationData = informationData + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 63a34fe713..f96ee7eee2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -26,6 +26,7 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericHeaderItem import im.vector.app.core.ui.list.genericItem @@ -49,7 +50,8 @@ class ViewEditHistoryEpoxyController @Inject constructor( private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val eventHtmlRenderer: EventHtmlRenderer, - private val dateFormatter: VectorDateFormatter + private val dateFormatter: VectorDateFormatter, + private val clock: Clock, ) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { @@ -86,7 +88,7 @@ class ViewEditHistoryEpoxyController @Inject constructor( val evDate = Calendar.getInstance().apply { timeInMillis = timelineEvent.originServerTs - ?: System.currentTimeMillis() + ?: clock.epochMillis() } if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) { // need to display header with day diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 080a79829a..aca2aab174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -92,7 +92,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde 2, eventIdToHighlight, partialState.rootThreadEventId, - partialState.isFromThreadTimeline()) + partialState.isFromThreadTimeline() + ) return if (mergedEvents.isEmpty()) { null } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index e4522b260e..b960e2c6a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -31,6 +31,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -142,6 +143,7 @@ class MessageItemFactory @Inject constructor( private val lightweightSettingsStorage: LightweightSettingsStorage, private val spanUtils: SpanUtils, private val session: Session, + private val clock: Clock, private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker, private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, @@ -207,15 +209,15 @@ class MessageItemFactory @Inject constructor( is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) - is MessageLocationContent -> { + is MessageLocationContent -> { if (vectorPreferences.labsRenderLocationsInTimeline()) { buildLocationItem(messageContent, informationData, highlight, attributes) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } - is MessageBeaconInfoContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes) - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageBeaconInfoContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes) + else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { layout(informationData.messageLayout.layoutRes) @@ -457,6 +459,7 @@ class MessageItemFactory @Inject constructor( reactionsSummaryEvents = attributes.reactionsSummaryEvents, ) ) + .clock(clock) .callback(callback) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) @@ -663,27 +666,31 @@ class MessageItemFactory @Inject constructor( ForegroundColorSpan(color), editStart, editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) // Note: text size is set to 14sp spannable.setSpan( AbsoluteSizeSpan(dimensionConverter.spToPx(13)), editStart, editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) - spannable.setSpan(object : ClickableSpan() { - override fun onClick(widget: View) { - callback?.onEditedDecorationClicked(informationData) - } + spannable.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + callback?.onEditedDecorationClicked(informationData) + } - override fun updateDrawState(ds: TextPaint) { - // nop - } - }, + override fun updateDrawState(ds: TextPaint) { + // nop + } + }, editStart, editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) return spannable } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index b5d620658e..f4bcc1ba65 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -50,13 +50,15 @@ class TimelineItemFactory @Inject constructor( timelineEvent = event, highlightedEventId = params.highlightedEventId, isFromThreadTimeline = params.isFromThreadTimeline(), - rootThreadEventId = params.rootThreadEventId)) { + rootThreadEventId = params.rootThreadEventId + )) { return buildEmptyItem( event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId, - params.isFromThreadTimeline()) + params.isFromThreadTimeline() + ) } // Manage state event differently, to check validity @@ -151,7 +153,8 @@ class TimelineItemFactory @Inject constructor( params.prevEvent, params.highlightedEventId, params.rootThreadEventId, - params.isFromThreadTimeline()) + params.isFromThreadTimeline() + ) } private fun buildEmptyItem(timelineEvent: TimelineEvent, @@ -163,7 +166,8 @@ class TimelineItemFactory @Inject constructor( timelineEvent = prevEvent, highlightedEventId = highlightedEventId, isFromThreadTimeline = isFromThreadTimeline, - rootThreadEventId = rootThreadEventId) + rootThreadEventId = rootThreadEventId + ) return TimelineEmptyItem_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 51dc26247c..7ad0cb27c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -276,11 +276,15 @@ class NoticeEventFormatter @Inject constructor( val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) return if (event.isSentByCurrentUser()) { - sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, - historyVisibilitySuffix) + sp.getString( + if (isDm) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, + historyVisibilitySuffix + ) } else { - sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, - senderName, historyVisibilitySuffix) + sp.getString( + if (isDm) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, + senderName, historyVisibilitySuffix + ) } } @@ -298,20 +302,27 @@ class NoticeEventFormatter @Inject constructor( } else { R.string.notice_room_third_party_revoked_invite_by_you }, - prevContent.displayName) + prevContent.displayName + ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite, - senderName, prevContent.displayName) + sp.getString( + if (isDm) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite, + senderName, prevContent.displayName + ) } } content != null -> { // Invitation case if (event.isSentByCurrentUser()) { - sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you, - content.displayName) + sp.getString( + if (isDm) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you, + content.displayName + ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite, - senderName, content.displayName) + sp.getString( + if (isDm) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite, + senderName, content.displayName + ) } } else -> null @@ -416,19 +427,21 @@ class NoticeEventFormatter @Inject constructor( return buildString { // Title - append(if (prevEventContent == null) { - if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_server_acl_set_title_by_you) - } else { - sp.getString(R.string.notice_room_server_acl_set_title, senderName) - } - } else { - if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_server_acl_updated_title_by_you) - } else { - sp.getString(R.string.notice_room_server_acl_updated_title, senderName) - } - }) + append( + if (prevEventContent == null) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_set_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_set_title, senderName) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_updated_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_updated_title, senderName) + } + } + ) if (eventContent.allowList.isEmpty()) { // Special case for stuck room appendNl(sp.getString(R.string.notice_room_server_acl_allow_is_empty)) @@ -562,8 +575,10 @@ class NoticeEventFormatter @Inject constructor( if (isDm) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join, - senderName) + sp.getString( + if (isDm) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join, + senderName + ) } GuestAccess.Forbidden -> if (event.isSentByCurrentUser()) { @@ -571,8 +586,10 @@ class NoticeEventFormatter @Inject constructor( if (isDm) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden, - senderName) + sp.getString( + if (isDm) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden, + senderName + ) } else -> null } @@ -705,18 +722,24 @@ class NoticeEventFormatter @Inject constructor( Membership.JOIN -> eventContent.safeReason?.let { reason -> if (event.isSentByCurrentUser()) { - sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you, - reason) + sp.getString( + if (isDm) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you, + reason + ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason, - senderDisplayName, reason) + sp.getString( + if (isDm) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason, + senderDisplayName, reason + ) } } ?: run { if (event.isSentByCurrentUser()) { sp.getString(if (isDm) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you) } else { - sp.getString(if (isDm) R.string.notice_direct_room_join else R.string.notice_room_join, - senderDisplayName) + sp.getString( + if (isDm) R.string.notice_direct_room_join else R.string.notice_room_join, + senderDisplayName + ) } } Membership.LEAVE -> @@ -745,15 +768,19 @@ class NoticeEventFormatter @Inject constructor( reason ) } else { - sp.getString(if (isDm) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason, - senderDisplayName, reason) + sp.getString( + if (isDm) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason, + senderDisplayName, reason + ) } } ?: run { if (event.isSentByCurrentUser()) { sp.getString(if (isDm) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you) } else { - sp.getString(if (isDm) R.string.notice_direct_room_leave else R.string.notice_room_leave, - senderDisplayName) + sp.getString( + if (isDm) R.string.notice_direct_room_leave else R.string.notice_room_leave, + senderDisplayName + ) } } } @@ -824,8 +851,10 @@ class NoticeEventFormatter @Inject constructor( if (event.isSentByCurrentUser()) { sp.getString(if (isDm) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you) } else { - sp.getString(if (isDm) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite, - senderName) + sp.getString( + if (isDm) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite, + senderName + ) } RoomJoinRules.PUBLIC -> if (event.isSentByCurrentUser()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt index 14769bc95b..c1ba085fd7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt @@ -25,20 +25,24 @@ class RoomHistoryVisibilityFormatter @Inject constructor( private val stringProvider: StringProvider ) { fun getNoticeSuffix(roomHistoryVisibility: RoomHistoryVisibility): String { - return stringProvider.getString(when (roomHistoryVisibility) { - RoomHistoryVisibility.WORLD_READABLE -> R.string.notice_room_visibility_world_readable - RoomHistoryVisibility.SHARED -> R.string.notice_room_visibility_shared - RoomHistoryVisibility.INVITED -> R.string.notice_room_visibility_invited - RoomHistoryVisibility.JOINED -> R.string.notice_room_visibility_joined - }) + return stringProvider.getString( + when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.notice_room_visibility_world_readable + RoomHistoryVisibility.SHARED -> R.string.notice_room_visibility_shared + RoomHistoryVisibility.INVITED -> R.string.notice_room_visibility_invited + RoomHistoryVisibility.JOINED -> R.string.notice_room_visibility_joined + } + ) } fun getSetting(roomHistoryVisibility: RoomHistoryVisibility): String { - return stringProvider.getString(when (roomHistoryVisibility) { - RoomHistoryVisibility.WORLD_READABLE -> R.string.room_settings_read_history_entry_anyone - RoomHistoryVisibility.SHARED -> R.string.room_settings_read_history_entry_members_only_option_time_shared - RoomHistoryVisibility.INVITED -> R.string.room_settings_read_history_entry_members_only_invited - RoomHistoryVisibility.JOINED -> R.string.room_settings_read_history_entry_members_only_joined - }) + return stringProvider.getString( + when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.room_settings_read_history_entry_anyone + RoomHistoryVisibility.SHARED -> R.string.room_settings_read_history_entry_members_only_option_time_shared + RoomHistoryVisibility.INVITED -> R.string.room_settings_read_history_entry_members_only_invited + RoomHistoryVisibility.JOINED -> R.string.room_settings_read_history_entry_members_only_joined + } + ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 9ff8ddfbce..b8882b3f47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -156,9 +156,11 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressBar.isIndeterminate = false progressBar.progress = percent.toInt() progressTextView.isVisible = true - progressTextView.text = progressLayout.context.getString(resId, + progressTextView.text = progressLayout.context.getString( + resId, TextUtils.formatFileSize(progressLayout.context, current, true), - TextUtils.formatFileSize(progressLayout.context, total, true)) + TextUtils.formatFileSize(progressLayout.context, total, true) + ) progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 8a0e1e18fd..e9361e564c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -25,6 +25,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ import org.matrix.android.sdk.api.session.room.timeline.Timeline +import kotlin.random.Random import kotlin.reflect.KMutableProperty0 private const val DEFAULT_PREFETCH_THRESHOLD = 30 @@ -104,7 +105,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut .coerceAtLeast(0) val loadingItem = LoadingItem_() - .id("prefetch_backward_loading${System.currentTimeMillis()}") + .id("prefetch_backward_loading${Random.nextLong()}") .showLoader(false) .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback) @@ -120,7 +121,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut .coerceAtLeast(0) val loadingItem = LoadingItem_() - .id("prefetch_forward_loading${System.currentTimeMillis()}") + .id("prefetch_forward_loading${Random.nextLong()}") .showLoader(false) .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback) add(indexOfPrefetchForward, loadingItem) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt index 0e6530fdca..fc4c55d1f3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -31,6 +31,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick +import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.MessageColorProvider @@ -47,6 +48,9 @@ abstract class VerificationRequestItem : AbsBaseMessageItem false msgType == MessageType.MSGTYPE_LOCATION -> vectorPreferences.labsRenderLocationsInTimeline() - else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE + else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 87ed9243a5..c9665a9125 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -91,7 +91,8 @@ class MessageBubbleView @JvmOverloads constructor( background = RippleDrawable( ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), bubbleDrawable, - rippleMaskDrawable) + rippleMaskDrawable + ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt index 0e4aebecfc..50eecb90fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt @@ -60,7 +60,7 @@ class MigrateRoomViewModel @AssistedInject constructor( override fun handle(action: MigrateRoomAction) { when (action) { - is MigrateRoomAction.SetAutoInvite -> { + is MigrateRoomAction.SetAutoInvite -> { setState { copy(shouldIssueInvites = action.autoInvite) } @@ -70,7 +70,7 @@ class MigrateRoomViewModel @AssistedInject constructor( copy(shouldUpdateKnownParents = action.update) } } - MigrateRoomAction.UpgradeRoom -> { + MigrateRoomAction.UpgradeRoom -> { handleUpgradeRoom() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 426ceb5024..2be6130a5d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -481,7 +481,8 @@ class RoomListFragment @Inject constructor( StateView.State.Empty( title = getString(R.string.room_list_catchup_empty_title), image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper), - message = getString(R.string.room_list_catchup_empty_body)) + message = getString(R.string.room_list_catchup_empty_body) + ) } RoomListDisplayMode.PEOPLE -> StateView.State.Empty( diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index 0a31987ae5..80c7b4e921 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -197,7 +197,8 @@ class RoomListSectionBuilderGroup( actualGroupId: String? ) { if (autoAcceptInvites.showInvites()) { - addSection(sections, + addSection( + sections, activeSpaceAwareQueries, R.string.invitations_header, true diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt index 625118919b..21fbd8b914 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -54,11 +54,13 @@ sealed class RoomListQuickActionsSharedAction( data class LowPriority(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_low_priority_add, - R.drawable.ic_low_priority_24) + R.drawable.ic_low_priority_24 + ) data class Favorite(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_favorite_add, - R.drawable.ic_star_24dp) + R.drawable.ic_star_24dp + ) data class Leave(val roomId: String, val showIcon: Boolean = true) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_leave, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index 726138ed93..13a12106c7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -76,7 +76,8 @@ class ThreadsActivity : VectorBaseActivity() { replaceFragment( views.threadsActivityFragmentContainer, ThreadListFragment::class.java, - threadListArgs) + threadListArgs + ) } private fun initThreadTimelineFragment(threadTimelineArgs: ThreadTimelineArgs) = @@ -87,7 +88,8 @@ class ThreadsActivity : VectorBaseActivity() { roomId = threadTimelineArgs.roomId, eventId = getEventIdToNavigate(), threadTimelineArgs = threadTimelineArgs - )) + ) + ) /** * This function is used to navigate to the selected thread timeline. @@ -100,7 +102,8 @@ class ThreadsActivity : VectorBaseActivity() { R.anim.animation_slide_in_right, R.anim.animation_slide_out_left, R.anim.animation_slide_in_left, - R.anim.animation_slide_out_right) + R.anim.animation_slide_out_right + ) } addFragmentToBackstack( container = views.threadsActivityFragmentContainer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt index ca948e6fdb..6cc72a4045 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt @@ -33,13 +33,17 @@ class TypingHelper @Inject constructor(private val stringProvider: StringProvide typingUsers.size == 1 -> stringProvider.getString(R.string.room_one_user_is_typing, typingUsers[0].disambiguatedDisplayName) typingUsers.size == 2 -> - stringProvider.getString(R.string.room_two_users_are_typing, + stringProvider.getString( + R.string.room_two_users_are_typing, typingUsers[0].disambiguatedDisplayName, - typingUsers[1].disambiguatedDisplayName) + typingUsers[1].disambiguatedDisplayName + ) else -> - stringProvider.getString(R.string.room_many_users_are_typing, + stringProvider.getString( + R.string.room_many_users_are_typing, typingUsers[0].disambiguatedDisplayName, - typingUsers[1].disambiguatedDisplayName) + typingUsers[1].disambiguatedDisplayName + ) } } @@ -47,10 +51,14 @@ class TypingHelper @Inject constructor(private val stringProvider: StringProvide return when { typingUsers.isEmpty() -> "" typingUsers.size == 1 -> typingUsers[0].disambiguatedDisplayName - typingUsers.size == 2 -> stringProvider.getString(R.string.room_notification_two_users_are_typing, - typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName) - else -> stringProvider.getString(R.string.room_notification_more_than_two_users_are_typing, - typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName) + typingUsers.size == 2 -> stringProvider.getString( + R.string.room_notification_two_users_are_typing, + typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName + ) + else -> stringProvider.getString( + R.string.room_notification_more_than_two_users_are_typing, + typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt index 5f1c709082..e815b7b0f3 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt @@ -67,9 +67,13 @@ object ServerUrlsRepository { fun getLastHomeServerUrl(context: Context): String { val prefs = DefaultSharedPreferences.getInstance(context) - return prefs.getString(HOME_SERVER_URL_PREF, - prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, - getDefaultHomeServerUrl(context))!!)!! + return prefs.getString( + HOME_SERVER_URL_PREF, + prefs.getString( + DEFAULT_REFERRER_HOME_SERVER_URL_PREF, + getDefaultHomeServerUrl(context) + )!! + )!! } /** diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index 776a887d5d..cad46e06a1 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -69,15 +69,21 @@ class InviteUsersToRoomViewModel @AssistedInject constructor( } .collect { val successMessage = when (selections.size) { - 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, - selections.first().getBestName()) - 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, + 1 -> stringProvider.getString( + R.string.invitation_sent_to_one_user, + selections.first().getBestName() + ) + 2 -> stringProvider.getString( + R.string.invitations_sent_to_two_users, selections.first().getBestName(), - selections.last().getBestName()) - else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, + selections.last().getBestName() + ) + else -> stringProvider.getQuantityString( + R.plurals.invitations_sent_to_one_and_more_users, selections.size - 1, selections.first().getBestName(), - selections.size - 1) + selections.size - 1 + ) } _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index c5cfe1ec75..231de00094 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -161,11 +161,13 @@ class LocationSharingViewModel @AssistedInject constructor( } private fun handleStartLiveLocationSharingAction(durationMillis: Long) { - _viewEvents.post(LocationSharingViewEvents.StartLiveLocationService( - sessionId = session.sessionId, - roomId = room.roomId, - durationMillis = durationMillis - )) + _viewEvents.post( + LocationSharingViewEvents.StartLiveLocationService( + sessionId = session.sessionId, + roomId = room.roomId, + durationMillis = durationMillis + ) + ) } override fun onLocationUpdate(locationData: LocationData) { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 79d2d37c44..d4730ecc8b 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -119,7 +119,8 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. // This way it will be automatically popped in when starting the next registration stage - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption @@ -150,25 +151,33 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) is LoginViewEvents.OnLoginFlowRetrieved -> - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginSignUpSignInSelectionFragment::class.java, - option = commonOption) + option = commonOption + ) is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) is LoginViewEvents.OnForgetPasswordClicked -> - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordFragment::class.java, - option = commonOption) + option = commonOption + ) is LoginViewEvents.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { // Go back to the login fragment @@ -177,20 +186,24 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA is LoginViewEvents.OnSendEmailSuccess -> { // Pop the enter email Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginWaitForEmailFragment::class.java, LoginWaitForEmailFragmentArgument(loginViewEvents.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents.OnSendMsisdnSuccess -> { // Pop the enter Msisdn Fragment supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents.Failure, is LoginViewEvents.Loading -> @@ -234,9 +247,11 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA when (loginViewEvents.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.EMS, - ServerType.Other -> addFragmentToBackstack(views.loginFragmentContainer, + ServerType.Other -> addFragmentToBackstack( + views.loginFragmentContainer, LoginServerUrlFormFragment::class.java, - option = commonOption) + option = commonOption + ) ServerType.Unknown -> Unit /* Should not happen */ } } @@ -254,17 +269,21 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA LoginMode.Unknown, is LoginMode.Sso -> error("Developer error") is LoginMode.SsoAndPassword, - LoginMode.Password -> addFragmentToBackstack(views.loginFragmentContainer, + LoginMode.Password -> addFragmentToBackstack( + views.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, - option = commonOption) + option = commonOption + ) LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) } } - SignMode.SignInWithMatrixId -> addFragmentToBackstack(views.loginFragmentContainer, + SignMode.SignInWithMatrixId -> addFragmentToBackstack( + views.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, - option = commonOption) + option = commonOption + ) } } @@ -288,9 +307,11 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA .setTitle(R.string.app_name) .setMessage(getString(R.string.login_registration_not_supported)) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginWebFragment::class.java, - option = commonOption) + option = commonOption + ) } .setNegativeButton(R.string.no, null) .show() @@ -301,9 +322,11 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA .setTitle(R.string.app_name) .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + addFragmentToBackstack( + views.loginFragmentContainer, LoginWebFragment::class.java, - option = commonOption) + option = commonOption + ) } .setNegativeButton(R.string.no, null) .show() @@ -331,26 +354,34 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.ReCaptcha -> addFragmentToBackstack( + views.loginFragmentContainer, LoginCaptchaFragment::class.java, LoginCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Email -> addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Email -> addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Msisdn -> addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Msisdn -> addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Terms -> addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Terms -> addFragmentToBackstack( + views.loginFragmentContainer, LoginTermsFragment::class.java, LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) else -> Unit // Should not happen } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt index 45b6e5b8cd..76f95d75e8 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt @@ -157,12 +157,14 @@ class LoginCaptchaFragment @Inject constructor( } } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { @Suppress("DEPRECATION") super.onReceivedError(view, errorCode, description, failingUrl) onError(description) } + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (url?.startsWith("js:") == true) { var json = url.substring(3) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 49198087d9..14587b7c09 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -117,11 +117,13 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint - SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint - }) + views.loginFieldTil.hint = getString( + when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint + } + ) // Handle direct signin first if (state.signMode == SignMode.SignInWithMatrixId) { @@ -215,12 +221,14 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn, - SignMode.SignInWithMatrixId -> R.string.login_signin - }) + views.loginSubmit.text = getString( + when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin + } + ) } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index e2fcb2fd12..ca9582b44b 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -100,11 +100,13 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment(), GalleryOrCameraDialogHelper.Listener { private val viewModel: AccountCreatedViewModel by fragmentViewModel() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding { return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false) @@ -73,7 +75,7 @@ class AccountCreatedFragment @Inject constructor( viewModel.onEach { invalidateState(it) } - views.loginAccountCreatedTime.text = dateFormatter.format(System.currentTimeMillis(), DateFormatKind.MESSAGE_SIMPLE) + views.loginAccountCreatedTime.text = dateFormatter.format(clock.epochMillis(), DateFormatKind.MESSAGE_SIMPLE) } private fun observeViewEvents() { @@ -113,9 +115,11 @@ class AccountCreatedFragment @Inject constructor( override fun onImageReady(uri: Uri?) { uri ?: return - viewModel.handle(AccountCreatedAction.SetAvatar( - avatarUri = uri, - filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + viewModel.handle( + AccountCreatedAction.SetAvatar( + avatarUri = uri, + filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) ) } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 57e48cac6d..8c685b8178 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -84,7 +84,8 @@ class MatrixToBottomSheet : private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { - replace(views.matrixToCardFragmentContainer.id, + replace( + views.matrixToCardFragmentContainer.id, fragmentClass.java, bundle, fragmentClass.simpleName diff --git a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt index 2f71089a39..b72b36a564 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt @@ -140,7 +140,8 @@ class SpaceCardRenderer @Inject constructor( avatarRenderer.render(item, images[index]) } inCard.peopleYouMayKnowText.setTextOrHide( - stringProvider.getQuantityString(R.plurals.space_people_you_know, + stringProvider.getQuantityString( + R.plurals.space_people_you_know, peopleYouKnow.count(), peopleYouKnow.count() ) diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index cd868c9f2f..e18a13a3e6 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -169,7 +169,8 @@ class RoomEventsAttachmentProvider( fileName = messageContent.body, mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index 4e6d19686b..c7bb54fcf4 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -87,7 +87,8 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc fileName = data.filename, mimeType = data.mimeType, url = data.url, - elementToDecrypt = data.elementToDecrypt) + elementToDecrypt = data.elementToDecrypt + ) } withContext(Dispatchers.Main) { result.fold( @@ -130,7 +131,8 @@ class VideoContentRenderer @Inject constructor(private val localFilesHelper: Loc fileName = data.filename, mimeType = data.mimeType, url = data.url, - elementToDecrypt = null) + elementToDecrypt = null + ) } withContext(Dispatchers.Main) { result.fold( diff --git a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt index b0401ccd30..6a24ac4fd7 100644 --- a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.intent.getMimeTypeFromUri +import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.features.notifications.NotificationUtils import kotlinx.coroutines.withContext @@ -30,7 +31,8 @@ import javax.inject.Inject class DownloadMediaUseCase @Inject constructor( @ApplicationContext private val appContext: Context, private val session: Session, - private val notificationUtils: NotificationUtils + private val notificationUtils: NotificationUtils, + private val clock: Clock, ) { suspend fun execute(input: File): Result = withContext(session.coroutineDispatchers.io) { @@ -40,7 +42,8 @@ class DownloadMediaUseCase @Inject constructor( file = input, title = input.name, mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 94aee1ba1a..7cc42ec57f 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -254,7 +254,8 @@ class DefaultNavigator @Inject constructor( val pr = session.cryptoService().verificationService().requestKeyVerification( supportedVerificationMethodsProvider.provide(), session.myUserId, - otherSessions) + otherSessions + ) VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId) .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) } else { @@ -525,12 +526,14 @@ class DefaultNavigator @Inject constructor( view: View, inMemory: List, options: ((MutableList>) -> Unit)?) { - VectorAttachmentViewerActivity.newIntent(activity, + VectorAttachmentViewerActivity.newIntent( + activity, mediaData, roomId, mediaData.eventId, inMemory, - ViewCompat.getTransitionName(view)).let { intent -> + ViewCompat.getTransitionName(view) + ).let { intent -> val pairs = ArrayList>() activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) @@ -599,24 +602,29 @@ class DefaultNavigator @Inject constructor( } override fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String?) { - context.startActivity(ThreadsActivity.newIntent( - context = context, - threadTimelineArgs = threadTimelineArgs, - threadListArgs = null, - eventIdToNavigate = eventIdToNavigate - )) + context.startActivity( + ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = threadTimelineArgs, + threadListArgs = null, + eventIdToNavigate = eventIdToNavigate + ) + ) } override fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) { - context.startActivity(ThreadsActivity.newIntent( - context = context, - threadTimelineArgs = null, - threadListArgs = ThreadListArgs( - roomId = threadTimelineArgs.roomId, - displayName = threadTimelineArgs.displayName, - avatarUrl = threadTimelineArgs.avatarUrl, - roomEncryptionTrustLevel = threadTimelineArgs.roomEncryptionTrustLevel - ))) + context.startActivity( + ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = null, + threadListArgs = ThreadListArgs( + roomId = threadTimelineArgs.roomId, + displayName = threadTimelineArgs.displayName, + avatarUrl = threadTimelineArgs.avatarUrl, + roomEncryptionTrustLevel = threadTimelineArgs.roomEncryptionTrustLevel + ) + ) + ) } override fun openScreenSharingPermissionDialog(screenCaptureIntent: Intent, diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 832f97bc4e..eb0735f2be 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -28,5 +28,6 @@ data class InviteNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, - override val isRedacted: Boolean = false + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false ) : NotifiableEvent diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index 52d8119cbb..a9ad79febf 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -27,4 +27,5 @@ sealed interface NotifiableEvent : Serializable { // Used to know if event should be replaced with the one coming from eventstream val canBeReplaced: Boolean val isRedacted: Boolean + val isUpdated: Boolean } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a1304bb6ae..80f5f47b3b 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -20,6 +20,7 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.takeAs import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter @@ -59,7 +60,8 @@ import javax.inject.Inject class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, private val noticeEventFormatter: NoticeEventFormatter, - private val displayableEventFormatter: DisplayableEventFormatter + private val displayableEventFormatter: DisplayableEventFormatter, + private val clock: Clock, ) { // private val eventDisplay = RiotEventDisplay(context) @@ -87,7 +89,7 @@ class NotifiableEventResolver @Inject constructor( eventId = event.eventId!!, editedEventId = timelineEvent.getEditedEventId(), noisy = false, // will be updated - timestamp = event.originServerTs ?: System.currentTimeMillis(), + timestamp = event.originServerTs ?: clock.epochMillis(), description = bodyPreview, title = stringProvider.getString(R.string.notification_unknown_new_event), soundName = null, @@ -179,15 +181,19 @@ class NotifiableEventResolver @Inject constructor( roomName = roomName, roomIsDirect = room.roomSummary()?.isDirect ?: false, roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail(room.roomSummary()?.avatarUrl, + .resolveThumbnail( + room.roomSummary()?.avatarUrl, 250, 250, - ContentUrlResolver.ThumbnailMethod.SCALE), + ContentUrlResolver.ThumbnailMethod.SCALE + ), senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderInfo.avatarUrl, + .resolveThumbnail( + event.senderInfo.avatarUrl, 250, 250, - ContentUrlResolver.ThumbnailMethod.SCALE), + ContentUrlResolver.ThumbnailMethod.SCALE + ), matrixID = session.myUserId, soundName = null ) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 35718666b0..d13e41daa8 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -38,7 +38,8 @@ data class NotifiableMessageEvent( // This is used for >N notification, as the result of a smart reply val outGoingMessage: Boolean = false, val outGoingMessageFailed: Boolean = false, - override val isRedacted: Boolean = false + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false ) : NotifiableEvent { val type: String = EventType.MESSAGE diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 0fe75ae1a0..4205f2ca5a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -23,6 +23,7 @@ import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.time.Clock import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom @@ -46,6 +47,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var analyticsTracker: AnalyticsTracker + @Inject lateinit var clock: Clock override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return @@ -138,7 +140,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { eventId = UUID.randomUUID().toString(), editedEventId = null, noisy = false, - timestamp = System.currentTimeMillis(), + timestamp = clock.epochMillis(), senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), senderId = session.myUserId, @@ -189,7 +191,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( event.eventId, false, - System.currentTimeMillis(), + clock.epochMillis(), session.myUser?.displayname ?: context?.getString(R.string.notification_sender_me), session.myUserId, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 62cac7507f..1243d3f798 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -155,7 +155,8 @@ class NotificationDrawerManager @Inject constructor( Timber.w(throwable, "refreshNotificationDrawerBg failure") } }, - canHandle.waitMillis()) + canHandle.waitMillis() + ) } @WorkerThread diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt index 04506b218b..7c933c389d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt @@ -112,7 +112,13 @@ data class NotificationEventQueue( private fun replace(replace: NotifiableEvent, with: NotifiableEvent) { queue.remove(replace) - queue.add(with) + queue.add( + when (with) { + is InviteNotifiableEvent -> with.copy(isUpdated = true) + is NotifiableMessageEvent -> with.copy(isUpdated = true) + is SimpleNotifiableEvent -> with.copy(isUpdated = true) + } + ) } fun clearMemberShipNotificationForRoom(roomId: String) { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index b95bbe1bf5..4b6815e7e4 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -96,7 +96,8 @@ class NotificationFactory @Inject constructor( invitationNotifications = invitationMeta, simpleNotifications = simpleMeta, useCompleteNotificationFormat = useCompleteNotificationFormat - )) + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index e44f42d5cd..78d771ee1c 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -51,6 +51,7 @@ import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.resources.StringProvider import im.vector.app.core.services.CallService +import im.vector.app.core.time.Clock import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver @@ -72,9 +73,12 @@ import kotlin.random.Random * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity */ @Singleton -class NotificationUtils @Inject constructor(private val context: Context, - private val stringProvider: StringProvider, - private val vectorPreferences: VectorPreferences) { +class NotificationUtils @Inject constructor( + private val context: Context, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { companion object { /* ========================================================================================== @@ -170,9 +174,11 @@ class NotificationUtils @Inject constructor(private val context: Context, * Default notification importance: shows everywhere, makes noise, but does not visually * intrude. */ - notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID, + notificationManager.createNotificationChannel(NotificationChannel( + NOISY_NOTIFICATION_CHANNEL_ID, stringProvider.getString(R.string.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, - NotificationManager.IMPORTANCE_DEFAULT) + NotificationManager.IMPORTANCE_DEFAULT + ) .apply { description = stringProvider.getString(R.string.notification_noisy_notifications) enableVibration(true) @@ -183,9 +189,11 @@ class NotificationUtils @Inject constructor(private val context: Context, /** * Low notification importance: shows everywhere, but is not intrusive. */ - notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID, + notificationManager.createNotificationChannel(NotificationChannel( + SILENT_NOTIFICATION_CHANNEL_ID, stringProvider.getString(R.string.notification_silent_notifications).ifEmpty { "Silent notifications" }, - NotificationManager.IMPORTANCE_LOW) + NotificationManager.IMPORTANCE_LOW + ) .apply { description = stringProvider.getString(R.string.notification_silent_notifications) setSound(null, null) @@ -193,18 +201,22 @@ class NotificationUtils @Inject constructor(private val context: Context, lightColor = accentColor }) - notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, + notificationManager.createNotificationChannel(NotificationChannel( + LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, stringProvider.getString(R.string.notification_listening_for_events).ifEmpty { "Listening for events" }, - NotificationManager.IMPORTANCE_MIN) + NotificationManager.IMPORTANCE_MIN + ) .apply { description = stringProvider.getString(R.string.notification_listening_for_events) setSound(null, null) setShowBadge(false) }) - notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, + notificationManager.createNotificationChannel(NotificationChannel( + CALL_NOTIFICATION_CHANNEL_ID, stringProvider.getString(R.string.call).ifEmpty { "Call" }, - NotificationManager.IMPORTANCE_HIGH) + NotificationManager.IMPORTANCE_HIGH + ) .apply { description = stringProvider.getString(R.string.call) setSound(null, null) @@ -262,11 +274,13 @@ class NotificationUtils @Inject constructor(private val context: Context, // reflection at runtime, to avoid compiler error: "Cannot resolve method.." try { val deprecatedMethod = notification.javaClass - .getMethod("setLatestEventInfo", + .getMethod( + "setLatestEventInfo", Context::class.java, CharSequence::class.java, CharSequence::class.java, - PendingIntent::class.java) + PendingIntent::class.java + ) deprecatedMethod.invoke(notification, context, stringProvider.getString(R.string.app_name), stringProvider.getString(subTitleResId), pi) } catch (ex: Exception) { Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=") @@ -323,7 +337,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } val contentPendingIntent = PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), contentIntent, PendingIntentCompat.FLAG_IMMUTABLE ) @@ -337,7 +351,7 @@ class NotificationUtils @Inject constructor(private val context: Context, mode = VectorCallActivity.INCOMING_ACCEPT ) ) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) @@ -386,13 +400,14 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentIntent = VectorCallActivity.newIntent( context = context, call = call, - mode = null).apply { + mode = null + ).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP data = createIgnoredUri(call.callId) } val contentPendingIntent = PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), contentIntent, PendingIntentCompat.FLAG_IMMUTABLE ) @@ -453,7 +468,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(VectorCallActivity.newIntent(context, call, null)) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) builder.setContentIntent(contentPendingIntent) @@ -467,7 +482,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } return PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -515,7 +530,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId))) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) builder.setContentIntent(contentPendingIntent) return builder.build() @@ -562,7 +577,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } PendingIntent.getActivity( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ).let { @@ -588,7 +603,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID return NotificationCompat.Builder(context, channelID) - .setOnlyAlertOnce(true) + .setOnlyAlertOnce(roomInfo.isUpdated) .setWhen(lastMessageTimestamp) // MESSAGING_STYLE sets title and content for API 16 and above devices. .setStyle(messageStyle) @@ -636,13 +651,15 @@ class NotificationUtils @Inject constructor(private val context: Context, markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) val markRoomReadPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), markRoomReadIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - NotificationCompat.Action.Builder(R.drawable.ic_material_done_all_white, - stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent) + NotificationCompat.Action.Builder( + R.drawable.ic_material_done_all_white, + stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent + ) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) .setShowsUserInterface(false) .build() @@ -654,8 +671,10 @@ class NotificationUtils @Inject constructor(private val context: Context, val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) .setLabel(stringProvider.getString(R.string.action_quick_reply)) .build() - NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, - stringProvider.getString(R.string.action_quick_reply), replyPendingIntent) + NotificationCompat.Action.Builder( + R.drawable.vector_notification_quick_reply, + stringProvider.getString(R.string.action_quick_reply), replyPendingIntent + ) .addRemoteInput(remoteInput) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) @@ -677,7 +696,7 @@ class NotificationUtils @Inject constructor(private val context: Context, intent.action = DISMISS_ROOM_NOTIF_ACTION val pendingIntent = PendingIntent.getBroadcast( context.applicationContext, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -712,7 +731,7 @@ class NotificationUtils @Inject constructor(private val context: Context, rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val rejectIntentPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -730,7 +749,7 @@ class NotificationUtils @Inject constructor(private val context: Context, joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val joinIntentPendingIntent = PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), joinIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) @@ -811,7 +830,7 @@ class NotificationUtils @Inject constructor(private val context: Context, .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(roomIntentTap) .getPendingIntent( - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) } @@ -844,7 +863,7 @@ class NotificationUtils @Inject constructor(private val context: Context, intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) return PendingIntent.getBroadcast( context, - System.currentTimeMillis().toInt(), + clock.epochMillis().toInt(), intent, // PendingIntents attached to actions with remote inputs must be mutable PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomEventGroupInfo.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomEventGroupInfo.kt index 64e40ed748..6ec4645382 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomEventGroupInfo.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomEventGroupInfo.kt @@ -31,4 +31,5 @@ data class RoomEventGroupInfo( var shouldBing: Boolean = false var customSound: String? = null var hasSmartReplyError: Boolean = false + var isUpdated: Boolean = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 8310c15daa..aa54176815 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -36,11 +36,12 @@ class RoomGroupMessageCreator @Inject constructor( val firstKnownRoomEvent = events[0] val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" val roomIsGroup = !firstKnownRoomEvent.roomIsDirect - val style = NotificationCompat.MessagingStyle(Person.Builder() - .setName(userDisplayName) - .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) - .setKey(firstKnownRoomEvent.matrixID) - .build() + val style = NotificationCompat.MessagingStyle( + Person.Builder() + .setName(userDisplayName) + .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) + .setKey(firstKnownRoomEvent.matrixID) + .build() ).also { it.conversationTitle = roomName.takeIf { roomIsGroup } it.isGroupConversation = roomIsGroup @@ -72,6 +73,7 @@ class RoomGroupMessageCreator @Inject constructor( it.hasSmartReplyError = smartReplyErrors.isNotEmpty() it.shouldBing = meta.shouldBing it.customSound = events.last().soundName + it.isUpdated = events.last().isUpdated }, largeIcon = largeBitmap, lastMessageTimestamp, diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 8c72372204..a58f22087d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -26,5 +26,6 @@ data class SimpleNotifiableEvent( val timestamp: Long, val soundName: String?, override var canBeReplaced: Boolean, - override val isRedacted: Boolean = false + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false ) : NotifiableEvent diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt index 91163434c2..7d1cb074ec 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -99,12 +99,16 @@ class SummaryGroupMessageCreator @Inject constructor( val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) if (messageNotificationCount > 0) { // Invitation and message - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - messageNotificationCount, messageNotificationCount) + val messageStr = stringProvider.getQuantityString( + R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount + ) if (roomCount > 1) { // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - roomCount, roomCount) + val roomStr = stringProvider.getQuantityString( + R.plurals.notification_unread_notified_messages_in_room_rooms, + roomCount, roomCount + ) stringProvider.getString( R.string.notification_unread_notified_messages_in_room_and_invitation, messageStr, @@ -125,8 +129,10 @@ class SummaryGroupMessageCreator @Inject constructor( } } else { // No invitation, only messages - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - messageNotificationCount, messageNotificationCount) + val messageStr = stringProvider.getQuantityString( + R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount + ) if (roomCount > 1) { // In several rooms val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt index 15d07a0197..9f63ff3e22 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt @@ -169,9 +169,11 @@ class Login2Variant( // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) is LoginViewEvents2.OpenHomeServerUrlFormScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginServerUrlFormFragment2::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { activity.addFragmentToBackstack(views.loginFragmentContainer, @@ -187,67 +189,87 @@ class Login2Variant( }) } is LoginViewEvents2.OpenSsoOnlyScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginSsoOnlyFragment2::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event) is LoginViewEvents2.OpenResetPasswordScreen -> - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordFragment2::class.java, - option = commonOption) + option = commonOption + ) is LoginViewEvents2.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment2::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginResetPasswordSuccessFragment2::class.java, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OnResetPasswordMailConfirmationSuccessDone -> { // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } is LoginViewEvents2.OnSendEmailSuccess -> - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragmentArgument(event.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) is LoginViewEvents2.OpenSigninPasswordScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginFragmentSigninPassword2::class.java, tag = FRAGMENT_LOGIN_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OpenSignupPasswordScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginFragmentSignupPassword2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginFragmentSignupUsername2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OpenSignInWithAnythingScreen -> { - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginFragmentToAny2::class.java, tag = FRAGMENT_LOGIN_TAG, - option = commonOption) + option = commonOption + ) } is LoginViewEvents2.OnSendMsisdnSuccess -> - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) is LoginViewEvents2.Failure -> // This is handled by the Fragments Unit @@ -268,9 +290,11 @@ class Login2Variant( if (event.newAccount) { // Propose to set avatar and display name // Back on this Fragment will finish the Activity - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, AccountCreatedFragment::class.java, - option = commonOption) + option = commonOption + ) } else { terminate(false) } @@ -321,9 +345,11 @@ class Login2Variant( .setTitle(R.string.app_name) .setMessage(activity.getString(R.string.login_registration_not_supported)) .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginWebFragment2::class.java, - option = commonOption) + option = commonOption + ) } .setNegativeButton(R.string.no, null) .show() @@ -334,9 +360,11 @@ class Login2Variant( .setTitle(R.string.app_name) .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) .setPositiveButton(R.string.yes) { _, _ -> - activity.addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginWebFragment2::class.java, - option = commonOption) + option = commonOption + ) } .setNegativeButton(R.string.no, null) .show() @@ -364,26 +392,34 @@ class Login2Variant( supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, + is Stage.ReCaptcha -> activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginCaptchaFragment2::class.java, LoginCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Email -> activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Msisdn -> activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, + option = commonOption + ) + is Stage.Terms -> activity.addFragmentToBackstack( + views.loginFragmentContainer, LoginTermsFragment2::class.java, LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) + option = commonOption + ) else -> Unit // Should not happen } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 82835849c0..25ae0327a8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -645,7 +645,7 @@ class OnboardingViewModel @AssistedInject constructor( when (awaitState().onboardingFlow) { OnboardingFlow.SignIn -> { updateSignMode(SignMode.SignIn) - internalRegisterAction(RegisterAction.StartRegistration, ::emitFlowResultViewEvent) + _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn)) } OnboardingFlow.SignUp -> { updateSignMode(SignMode.SignUp) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt index 558c26fe56..c4099effa7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt @@ -103,12 +103,14 @@ class CaptchaWebview @Inject constructor( } } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { @Suppress("DEPRECATION") super.onReceivedError(view, errorCode, description, failingUrl) onError(description) } + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (url?.startsWith("js:") == true) { val javascriptResponse = parseJsonFromUrl(url) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt index 49db52da67..b8114b5d94 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.animations.play import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.databinding.FragmentFtueAccountCreatedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents @@ -57,7 +58,7 @@ class FtueAuthAccountCreatedFragment @Inject constructor( views.personalizeButtonGroup.isVisible = canPersonalize views.takeMeHomeButtonGroup.isVisible = !canPersonalize - if (!hasPlayedConfetti && !canPersonalize) { + if (!hasPlayedConfetti && !canPersonalize && requireContext().isAnimationEnabled()) { hasPlayedConfetti = true views.viewKonfetti.isVisible = true views.viewKonfetti.play() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt index 81300932db..0ccff7dffc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt @@ -29,6 +29,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentFtueProfilePictureBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.onboarding.OnboardingAction @@ -39,10 +40,11 @@ import javax.inject.Inject class FtueAuthChooseProfilePictureFragment @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : AbstractFtueAuthFragment(), GalleryOrCameraDialogHelper.Listener { - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val avatarRenderer: AvatarRenderer by lazy { requireContext().singletonEntryPoint().avatarRenderer() } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueProfilePictureBinding { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index 4888b43946..696ebb4786 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -127,11 +127,13 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< // This can be called by the IME action, so deal with empty cases var error = 0 if (login.isEmpty()) { - views.loginFieldTil.error = getString(if (isSignupMode) { - R.string.error_empty_field_choose_user_name - } else { - R.string.error_empty_field_enter_user_name - }) + views.loginFieldTil.error = getString( + if (isSignupMode) { + R.string.error_empty_field_choose_user_name + } else { + R.string.error_empty_field_enter_user_name + } + ) error++ } if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { @@ -139,11 +141,13 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< error++ } if (password.isEmpty()) { - views.passwordFieldTil.error = getString(if (isSignupMode) { - R.string.error_empty_field_choose_password - } else { - R.string.error_empty_field_your_password - }) + views.passwordFieldTil.error = getString( + if (isSignupMode) { + R.string.error_empty_field_choose_password + } else { + R.string.error_empty_field_your_password + } + ) error++ } @@ -159,12 +163,14 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< } private fun setupUi(state: OnboardingViewState) { - views.loginFieldTil.hint = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint - SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint - }) + views.loginFieldTil.hint = getString( + when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint + } + ) // Handle direct signin first if (state.signMode == SignMode.SignInWithMatrixId) { @@ -225,12 +231,14 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< private fun setupButtons(state: OnboardingViewState) { views.forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn - views.loginSubmit.text = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn, - SignMode.SignInWithMatrixId -> R.string.login_signin - }) + views.loginSubmit.text = getString( + when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin + } + ) } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt index 6b47b9830c..074f58864e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt @@ -22,6 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import im.vector.app.core.animations.play +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.databinding.FragmentFtuePersonalizationCompleteBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents @@ -43,7 +44,7 @@ class FtueAuthPersonalizationCompleteFragment @Inject constructor() : AbstractFt private fun setupViews() { views.personalizationCompleteCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } - if (!hasPlayedConfetti) { + if (!hasPlayedConfetti && requireContext().isAnimationEnabled()) { hasPlayedConfetti = true views.viewKonfetti.isVisible = true views.viewKonfetti.play() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt index df304d028d..c542f80712 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthServerUrlFormFragment.kt @@ -104,11 +104,13 @@ class FtueAuthServerUrlFormFragment @Inject constructor() : AbstractFtueAuthFrag } } val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList() - views.loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter( - requireContext(), - R.layout.item_completion_homeserver, - completions - )) + views.loginServerUrlFormHomeServerUrl.setAdapter( + ArrayAdapter( + requireContext(), + R.layout.item_completion_homeserver, + completions + ) + ) views.loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU .takeIf { completions.isNotEmpty() } ?: TextInputLayout.END_ICON_NONE diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt index 26b9a38d83..aa30d11134 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt @@ -138,6 +138,7 @@ class FtueAuthWebFragment @Inject constructor( .show() } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) @@ -190,6 +191,7 @@ class FtueAuthWebFragment @Inject constructor( * @param url * @return */ + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt index 006492f6dc..23f7014374 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt @@ -41,32 +41,34 @@ class SplashCarouselStateFactory @Inject constructor( val lightTheme = themeProvider.isLightTheme() fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_carousel_page_dark fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable - return SplashCarouselState(listOf( - SplashCarouselState.Item( - R.string.ftue_auth_carousel_secure_title.colorTerminatingFullStop(R.attr.colorAccent), - R.string.ftue_auth_carousel_secure_body, - hero(R.drawable.ic_splash_conversations, R.drawable.ic_splash_conversations_dark), - background(R.drawable.bg_carousel_page_1) - ), - SplashCarouselState.Item( - R.string.ftue_auth_carousel_control_title.colorTerminatingFullStop(R.attr.colorAccent), - R.string.ftue_auth_carousel_control_body, - hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark), - background(R.drawable.bg_carousel_page_2) - ), - SplashCarouselState.Item( - R.string.ftue_auth_carousel_encrypted_title.colorTerminatingFullStop(R.attr.colorAccent), - R.string.ftue_auth_carousel_encrypted_body, - hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark), - background(R.drawable.bg_carousel_page_3) - ), - SplashCarouselState.Item( - collaborationTitle().colorTerminatingFullStop(R.attr.colorAccent), - R.string.ftue_auth_carousel_workplace_body, - hero(R.drawable.ic_splash_collaboration, R.drawable.ic_splash_collaboration_dark), - background(R.drawable.bg_carousel_page_4) + return SplashCarouselState( + listOf( + SplashCarouselState.Item( + R.string.ftue_auth_carousel_secure_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_secure_body, + hero(R.drawable.ic_splash_conversations, R.drawable.ic_splash_conversations_dark), + background(R.drawable.bg_carousel_page_1) + ), + SplashCarouselState.Item( + R.string.ftue_auth_carousel_control_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_control_body, + hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark), + background(R.drawable.bg_carousel_page_2) + ), + SplashCarouselState.Item( + R.string.ftue_auth_carousel_encrypted_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_encrypted_body, + hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark), + background(R.drawable.bg_carousel_page_3) + ), + SplashCarouselState.Item( + collaborationTitle().colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_workplace_body, + hero(R.drawable.ic_splash_collaboration, R.drawable.ic_splash_collaboration_dark), + background(R.drawable.bg_carousel_page_4) + ) ) - )) + ) } private fun collaborationTitle(): Int { diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index 26ff35cdbd..74b696510f 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -219,7 +219,8 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti displayName = roomSummary.displayName, avatarUrl = roomSummary.avatarUrl, roomEncryptionTrustLevel = roomSummary.roomEncryptionTrustLevel, - rootThreadEventId = rootThreadEventId) + rootThreadEventId = rootThreadEventId + ) navigator.openThread(context, threadTimelineArgs, eventId) } else { navigator.openRoom(context, roomId, eventId, buildTask) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt index f848a3174e..e41ea320ac 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber @@ -59,7 +60,7 @@ class PinLocker @Inject constructor( return liveState } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) private fun computeState() { GlobalScope.launch { val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index ae03b5345a..5d16fabbfd 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -24,7 +24,8 @@ import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import com.tapadoo.alerter.Alerter import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.utils.isAnimationDisabled +import im.vector.app.core.time.Clock +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.signout.hard.SignedOutActivity @@ -41,7 +42,9 @@ import javax.inject.Singleton * will be back in the queue in first position. */ @Singleton -class PopupAlertManager @Inject constructor() { +class PopupAlertManager @Inject constructor( + private val clock: Clock, +) { companion object { const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE @@ -116,7 +119,7 @@ class PopupAlertManager @Inject constructor() { return } if (currentAlerter != null) { - if (currentAlerter!!.expirationTimestamp != null && System.currentTimeMillis() > currentAlerter!!.expirationTimestamp!!) { + if (currentAlerter!!.expirationTimestamp != null && clock.epochMillis() > currentAlerter!!.expirationTimestamp!!) { // this alert has expired, remove it // perform dismiss try { @@ -162,7 +165,7 @@ class PopupAlertManager @Inject constructor() { currentAlerter = next next?.let { if (!shouldBeDisplayedIn(next, currentActivity)) return - val currentTime = System.currentTimeMillis() + val currentTime = clock.epochMillis() if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) { // skip try { @@ -215,7 +218,7 @@ class PopupAlertManager @Inject constructor() { if (!alert.isLight) { clearLightStatusBar() } - val noAnimation = !animate || isAnimationDisabled(activity) + val noAnimation = !(animate && activity.isAnimationEnabled()) alert.weakCurrentActivity = WeakReference(activity) val alerter = Alerter.create(activity, alert.layoutRes) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index f0edf6fd57..350dd13b22 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -176,16 +176,22 @@ class BugReportActivity : VectorBaseActivity() { if (!reason.isNullOrEmpty()) { when (reportType) { ReportType.BUG_REPORT -> { - Toast.makeText(this@BugReportActivity, - getString(R.string.send_bug_report_failed, reason), Toast.LENGTH_LONG).show() + Toast.makeText( + this@BugReportActivity, + getString(R.string.send_bug_report_failed, reason), Toast.LENGTH_LONG + ).show() } ReportType.SUGGESTION -> { - Toast.makeText(this@BugReportActivity, - getString(R.string.send_suggestion_failed, reason), Toast.LENGTH_LONG).show() + Toast.makeText( + this@BugReportActivity, + getString(R.string.send_suggestion_failed, reason), Toast.LENGTH_LONG + ).show() } ReportType.SPACE_BETA_FEEDBACK -> { - Toast.makeText(this@BugReportActivity, - getString(R.string.feedback_failed, reason), Toast.LENGTH_LONG).show() + Toast.makeText( + this@BugReportActivity, + getString(R.string.feedback_failed, reason), Toast.LENGTH_LONG + ).show() } else -> { // nop diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 8b514f4003..f723a281a0 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -111,7 +111,8 @@ class BugReporter @Inject constructor( private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - private val LOGCAT_CMD_ERROR = arrayOf("logcat", // /< Run 'logcat' command + private val LOGCAT_CMD_ERROR = arrayOf( + "logcat", // /< Run 'logcat' command "-d", // /< Dump the log rather than continue outputting it "-v", // formatting "threadtime", // include timestamps @@ -278,8 +279,10 @@ class BugReporter @Inject constructor( .addFormDataPart("device", Build.MODEL.trim()) .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) - .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + - Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) + .addFormDataPart( + "os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME + ) .addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) @@ -317,8 +320,10 @@ class BugReporter @Inject constructor( bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) } - builder.addFormDataPart("file", - logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + builder.addFormDataPart( + "file", + logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()) + ) } catch (e: Exception) { Timber.e(e, "## sendBugReport() : fail to write screenshot$e") } @@ -494,11 +499,13 @@ class BugReporter @Inject constructor( // app: Identifier for the application (eg 'riot-web'). // Should correspond to a mapping configured in the configuration file for github issue reporting to work. // (see R.string.bug_report_url for configured RS server) - return context.getString(when (reportType) { - ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name - else -> R.string.bug_report_app_name - }) + return context.getString( + when (reportType) { + ReportType.AUTO_UISI_SENDER, + ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name + else -> R.string.bug_report_app_name + } + ) } // ============================================================================================================== // crash report management diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt index 2f8e013f46..b687fa6a4d 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt @@ -19,6 +19,7 @@ package im.vector.app.features.rageshake import android.content.Context import android.util.Log import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -88,7 +89,7 @@ class VectorFileLogger @Inject constructor( } } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { fileHandler ?: return GlobalScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/features/reactions/widget/DotsView.kt b/vector/src/main/java/im/vector/app/features/reactions/widget/DotsView.kt index c9283c663a..7b0052db0b 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/widget/DotsView.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/widget/DotsView.kt @@ -126,10 +126,12 @@ class DotsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? this.currentDotSize2 = maxDotSize } else if (currentProgress < 0.5) { this.currentDotSize2 = CircleView.mapValueFromRangeToRange( - currentProgress, 0.2f, 0.5f, maxDotSize, 0.3f * maxDotSize) + currentProgress, 0.2f, 0.5f, maxDotSize, 0.3f * maxDotSize + ) } else { this.currentDotSize2 = CircleView.mapValueFromRangeToRange( - currentProgress, 0.5f, 1f, maxDotSize * 0.3f, 0f) + currentProgress, 0.5f, 1f, maxDotSize * 0.3f, 0f + ) } } @@ -143,17 +145,20 @@ class DotsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? private fun updateOuterDotsPosition() { if (currentProgress < 0.3f) { this.currentRadius1 = CircleView.mapValueFromRangeToRange( - currentProgress, 0.0f, 0.3f, 0f, maxOuterDotsRadius * 0.8f) + currentProgress, 0.0f, 0.3f, 0f, maxOuterDotsRadius * 0.8f + ) } else { this.currentRadius1 = CircleView.mapValueFromRangeToRange( - currentProgress, 0.3f, 1f, 0.8f * maxOuterDotsRadius, maxOuterDotsRadius) + currentProgress, 0.3f, 1f, 0.8f * maxOuterDotsRadius, maxOuterDotsRadius + ) } if (currentProgress < 0.7) { this.currentDotSize1 = maxDotSize } else { this.currentDotSize1 = CircleView.mapValueFromRangeToRange( - currentProgress, 0.7f, 1f, maxDotSize, 0f) + currentProgress, 0.7f, 1f, maxDotSize, 0f + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index e8540a6974..b2bfc53f96 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -166,7 +166,8 @@ class RoomDirectoryViewModel @AssistedInject constructor( currentJob = viewModelScope.launch { val data = try { - session.roomDirectoryService().getPublicRooms(roomDirectoryData.homeServer, + session.roomDirectoryService().getPublicRooms( + roomDirectoryData.homeServer, PublicRoomsParams( limit = PUBLIC_ROOMS_LIMIT, filter = PublicRoomsFilter(searchTerm = filter), diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index e4c350b88e..d5ea954b64 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -87,12 +87,14 @@ class CreateRoomActivity : VectorBaseActivity() { openAfterCreate: Boolean = true, currentSpaceId: String? = null): Intent { return Intent(context, CreateRoomActivity::class.java).apply { - putExtra(Mavericks.KEY_ARG, CreateRoomArgs( + putExtra( + Mavericks.KEY_ARG, CreateRoomArgs( initialName = initialName, isSpace = isSpace, openAfterCreate = openAfterCreate, parentSpaceId = currentSpaceId - )) + ) + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 7dd9cb4e00..fae88ed8a2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -146,7 +146,8 @@ class CreateRoomController @Inject constructor( hint(host.stringProvider.getString(R.string.room_alias_address_hint)) errorMessage( host.roomAliasErrorFormatter.format( - (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) + (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError + ) ) onTextChange { value -> host.listener?.setAliasLocalPart(value) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 891b1f6e89..82cc270458 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentCreateRoomBinding import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.navigation.Navigator @@ -63,7 +64,8 @@ data class CreateRoomArgs( class CreateRoomFragment @Inject constructor( private val createRoomController: CreateRoomController, private val createSpaceController: CreateSubSpaceController, - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : VectorBaseFragment(), CreateRoomController.Listener, GalleryOrCameraDialogHelper.Listener, @@ -75,7 +77,7 @@ class CreateRoomFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreateRoomBinding { return FragmentCreateRoomBinding.inflate(inflater, container, false) @@ -167,7 +169,8 @@ class CreateRoomFragment @Inject constructor( } else { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) } - RoomJoinRuleBottomSheet.newInstance(state.roomJoinRules, + RoomJoinRuleBottomSheet.newInstance( + state.roomJoinRules, allowed.map { it.toOption(false) }, state.isSubSpace, state.parentSpaceSummary?.displayName diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt index e67b272c32..6d2fc15a0b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt @@ -86,7 +86,8 @@ class CreateSubSpaceController @Inject constructor( maxLength(MatrixConstants.maxAliasLocalPartLength(data.homeServerName)) errorMessage( host.roomAliasErrorFormatter.format( - (((data.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) + (((data.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError + ) ) onTextChange { value -> host.listener?.setAliasLocalPart(value) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 545e9f7190..adc720b0a5 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -182,6 +182,15 @@ class RoomMemberProfileController @Inject constructor( // More buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + if (!state.isMine) { + buildProfileAction( + id = "direct", + editable = false, + title = stringProvider.getString(R.string.room_member_open_or_create_dm), + action = { callback?.onOpenDmClicked() } + ) + } + buildProfileAction( id = "overrideColor", editable = false, @@ -194,13 +203,6 @@ class RoomMemberProfileController @Inject constructor( if (!state.isMine) { val membership = state.asyncMembership() ?: return - buildProfileAction( - id = "direct", - editable = false, - title = stringProvider.getString(R.string.room_member_open_or_create_dm), - action = { callback?.onOpenDmClicked() } - ) - if (!state.isSpace && state.hasReadReceipt) { buildProfileAction( id = "read_receipt", diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 760bbe9353..032ab74153 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -111,7 +111,8 @@ class RoomMemberProfileFragment @Inject constructor( headerViews.memberProfileStateView.contentView = headerViews.memberProfileInfoContainer views.matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true, disableItemAnimation = true) roomMemberProfileController.callback = this - appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, + appBarStateChangeListener = MatrixItemAppBarStateChangeListener( + headerView, listOf( views.matrixProfileToolbarAvatarImageView, views.matrixProfileToolbarTitleView, diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 6d4ea45bac..fb1e222080 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -242,10 +242,12 @@ class RoomMemberProfileViewModel @AssistedInject constructor( if (state.isRoomEncrypted) { if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) { // ok, let's find or create the DM room - _viewEvents.post(RoomMemberProfileViewEvents.StartVerification( - userId = state.userId, - canCrossSign = session.cryptoService().crossSigningService().canCrossSign() - )) + _viewEvents.post( + RoomMemberProfileViewEvents.StartVerification( + userId = state.userId, + canCrossSign = session.cryptoService().crossSigningService().canCrossSign() + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt index 8df0b3ffd5..35f431db1d 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt @@ -96,7 +96,8 @@ class DeviceListBottomSheet : private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { - replace(R.id.bottomSheetFragmentContainer, + replace( + R.id.bottomSheetFragmentContainer, fragmentClass.java, bundle, fragmentClass.simpleName diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index c3991cef99..0e25ec5f8f 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -65,14 +65,22 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi apply { if (isVerified) { // TODO FORMAT - text(host.stringProvider.getString(R.string.verification_profile_device_verified_because, - data.userItem?.displayName ?: "", - data.userItem?.id ?: "").toEpoxyCharSequence()) + text( + host.stringProvider.getString( + R.string.verification_profile_device_verified_because, + data.userItem?.displayName ?: "", + data.userItem?.id ?: "" + ).toEpoxyCharSequence() + ) } else { // TODO what if mine - text(host.stringProvider.getString(R.string.verification_profile_device_new_signing, - data.userItem?.displayName ?: "", - data.userItem?.id ?: "").toEpoxyCharSequence()) + text( + host.stringProvider.getString( + R.string.verification_profile_device_new_signing, + data.userItem?.displayName ?: "", + data.userItem?.id ?: "" + ).toEpoxyCharSequence() + ) } } // text(stringProvider.getString(R.string.verification_profile_device_untrust_info)) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 372b4e5a70..06f56bff89 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -179,11 +179,13 @@ class RoomProfileController @Inject constructor( buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) buildProfileAction( id = "settings", - title = stringProvider.getString(if (roomSummary.isDirect) { - R.string.direct_room_profile_section_more_settings - } else { - R.string.room_profile_section_more_settings - }), + title = stringProvider.getString( + if (roomSummary.isDirect) { + R.string.direct_room_profile_section_more_settings + } else { + R.string.room_profile_section_more_settings + } + ), icon = R.drawable.ic_room_profile_settings, action = { callback?.onSettingsClicked() } ) @@ -228,11 +230,13 @@ class RoomProfileController @Inject constructor( } buildProfileAction( id = "leave", - title = stringProvider.getString(if (roomSummary.isDirect) { - R.string.direct_room_profile_section_more_leave - } else { - R.string.room_profile_section_more_leave - }), + title = stringProvider.getString( + if (roomSummary.isDirect) { + R.string.direct_room_profile_section_more_leave + } else { + R.string.room_profile_section_more_leave + } + ), divider = false, destructive = true, icon = R.drawable.ic_room_actions_leave, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 3944066584..7be92bfdcf 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -113,9 +113,11 @@ class RoomProfileFragment @Inject constructor( setupRecyclerView() appBarStateChangeListener = MatrixItemAppBarStateChangeListener( headerView, - listOf(views.matrixProfileToolbarAvatarImageView, + listOf( + views.matrixProfileToolbarAvatarImageView, views.matrixProfileToolbarTitleView, - views.matrixProfileDecorationToolbarAvatarImageView) + views.matrixProfileDecorationToolbarAvatarImageView + ) ) views.matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) roomProfileViewModel.observeViewEvents { @@ -269,11 +271,13 @@ class RoomProfileFragment @Inject constructor( override fun createShortcut() { // Ask the view model to prepare it... roomProfileViewModel.handle(RoomProfileAction.CreateShortcut) - analyticsTracker.capture(Interaction( - index = null, - interactionType = null, - name = Interaction.Name.MobileRoomAddHome - )) + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = Interaction.Name.MobileRoomAddHome + ) + ) } private fun addShortcut(onShortcutReady: RoomProfileViewEvents.OnShortcutReady) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index ec770f5af4..1428dc0cf0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -191,11 +191,13 @@ class RoomProfileViewModel @AssistedInject constructor( viewModelScope.launch { try { session.roomService().leaveRoom(room.roomId) - analyticsTracker.capture(Interaction( - index = null, - interactionType = null, - name = Interaction.Name.MobileRoomLeave - )) + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = Interaction.Name.MobileRoomLeave + ) + ) // Do nothing, we will be closing the room automatically when it will get back from sync } catch (failure: Throwable) { _viewEvents.post(RoomProfileViewEvents.Failure(failure)) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt index fcf6bc3a47..3db1e384bc 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -98,8 +98,12 @@ class RoomAliasController @Inject constructor( is Fail -> { errorWithRetryItem { id("rd_error") - text(host.stringProvider.getString(R.string.room_alias_publish_to_directory_error, - host.errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error))) + text( + host.stringProvider.getString( + R.string.room_alias_publish_to_directory_error, + host.errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error) + ) + ) listener { host.callback?.retry() } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt index 56dbcbfba4..6e4613c03c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt @@ -92,13 +92,15 @@ class RoomAliasBottomSheet : isLocal: Boolean, canEditCanonicalAlias: Boolean): RoomAliasBottomSheet { return RoomAliasBottomSheet().apply { - setArguments(RoomAliasBottomSheetArgs( - alias = alias, - isPublished = isPublished, - isMainAlias = isMainAlias, - isLocal = isLocal, - canEditCanonicalAlias = canEditCanonicalAlias - )) + setArguments( + RoomAliasBottomSheetArgs( + alias = alias, + isPublished = isPublished, + isMainAlias = isMainAlias, + isLocal = isLocal, + canEditCanonicalAlias = canEditCanonicalAlias + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt index 9590d1bbc3..fffae6159c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt @@ -132,12 +132,15 @@ class RoomPermissionsController @Inject constructor( settingsInfoItem { id("notice") - helperText(host.stringProvider.getString( - if (editable) { - if (isSpace) R.string.space_permissions_notice else R.string.room_permissions_notice - } else { - if (isSpace) R.string.space_permissions_notice_read_only else R.string.room_permissions_notice_read_only - })) + helperText( + host.stringProvider.getString( + if (editable) { + if (isSpace) R.string.space_permissions_notice else R.string.room_permissions_notice + } else { + if (isSpace) R.string.space_permissions_notice_read_only else R.string.room_permissions_notice_read_only + } + ) + ) } // Useful permissions diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index eb69e36ba0..85151e1258 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.analytics.plan.MobileScreen @@ -57,7 +58,8 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( private val controller: RoomSettingsController, colorProvider: ColorProvider, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + clock: Clock, ) : VectorBaseFragment(), RoomSettingsController.Callback, @@ -70,7 +72,7 @@ class RoomSettingsFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel private val roomProfileArgs: RoomProfileArgs by args() - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomSettingGenericBinding { return FragmentRoomSettingGenericBinding.inflate(inflater, container, false) @@ -198,7 +200,8 @@ class RoomSettingsFragment @Inject constructor( RoomSettingsAction.SetAvatarAction( RoomSettingsViewState.AvatarAction.UpdateAvatar( newAvatarUri = uri, - newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) ) ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 2cbe75ae56..ee04d22ddc 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -90,12 +90,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState::newTopic, RoomSettingsViewState::newHistoryVisibility, RoomSettingsViewState::newRoomJoinRules, - RoomSettingsViewState::roomSummary) { avatarAction, - newName, - newTopic, - newHistoryVisibility, - newJoinRule, - asyncSummary -> + RoomSettingsViewState::roomSummary + ) { avatarAction, + newName, + newTopic, + newHistoryVisibility, + newJoinRule, + asyncSummary -> val summary = asyncSummary() setState { copy( @@ -130,14 +131,22 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), - canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_HISTORY_VISIBILITY), - canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_JOIN_RULES) && - powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_GUEST_ACCESS), - canAddChildren = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_SPACE_CHILD) + canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend( + session.myUserId, true, + EventType.STATE_ROOM_HISTORY_VISIBILITY + ), + canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend( + session.myUserId, true, + EventType.STATE_ROOM_JOIN_RULES + ) && + powerLevelsHelper.isUserAllowedToSend( + session.myUserId, true, + EventType.STATE_ROOM_GUEST_ACCESS + ), + canAddChildren = powerLevelsHelper.isUserAllowedToSend( + session.myUserId, true, + EventType.STATE_SPACE_CHILD + ) ) setState { copy(actionPermissions = permissions) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt index 7099d30862..d6e74c546d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -121,7 +121,8 @@ class RoomJoinRuleActivity : VectorBaseActivity() { supportFragmentManager.commitTransaction { setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) val tag = RoomJoinRuleChooseRestrictedFragment::class.simpleName - replace(views.simpleFragmentContainer.id, + replace( + views.simpleFragmentContainer.id, RoomJoinRuleChooseRestrictedFragment::class.java, this@RoomJoinRuleActivity.roomProfileArgs.toMvRxBundle(), tag diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 6a115ad272..721f99178f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -30,6 +30,7 @@ import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.R import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.time.Clock import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.shareMedia import im.vector.app.databinding.FragmentRoomUploadsBinding @@ -43,7 +44,8 @@ import javax.inject.Inject class RoomUploadsFragment @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val notificationUtils: NotificationUtils + private val notificationUtils: NotificationUtils, + private val clock: Clock, ) : VectorBaseFragment() { private val roomProfileArgs: RoomProfileArgs by args() @@ -88,7 +90,8 @@ class RoomUploadsFragment @Inject constructor( file = it.file, title = it.title, mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), - notificationUtils = notificationUtils + notificationUtils = notificationUtils, + currentTimeMillis = clock.epochMillis() ) }.onFailure { failure -> if (!isAdded) return@onFailure diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt index 5edca79699..9ea6fd2dc2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -117,7 +117,8 @@ class RoomUploadsViewModel @AssistedInject constructor( viewModelScope.launch { val event = try { val file = session.fileService().downloadFile( - messageContent = action.uploadEvent.contentWithAttachmentContent) + messageContent = action.uploadEvent.contentWithAttachmentContent + ) RoomUploadsViewEvents.FileReadyForSharing(file) } catch (failure: Throwable) { RoomUploadsViewEvents.Failure(failure) @@ -130,7 +131,8 @@ class RoomUploadsViewModel @AssistedInject constructor( viewModelScope.launch { val event = try { val file = session.fileService().downloadFile( - messageContent = action.uploadEvent.contentWithAttachmentContent) + messageContent = action.uploadEvent.contentWithAttachmentContent + ) RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body) } catch (failure: Throwable) { RoomUploadsViewEvents.Failure(failure) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt index 4d45c2ccfc..b741b5c652 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileController.kt @@ -68,9 +68,13 @@ class UploadsFileController @Inject constructor( uploadsFileItem { id(uploadEvent.eventId) title(uploadEvent.contentWithAttachmentContent.body) - subtitle(host.stringProvider.getString(R.string.uploads_files_subtitle, - uploadEvent.senderInfo.disambiguatedDisplayName, - host.dateFormatter.format(uploadEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME))) + subtitle( + host.stringProvider.getString( + R.string.uploads_files_subtitle, + uploadEvent.senderInfo.disambiguatedDisplayName, + host.dateFormatter.format(uploadEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + ) + ) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { host.listener?.onOpenClicked(uploadEvent) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt index f558ba28c6..1c67b86493 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt @@ -62,7 +62,8 @@ object VectorLocale { val preferences = DefaultSharedPreferences.getInstance(context) if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { - applicationLocale = Locale(preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, + applicationLocale = Locale( + preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, preferences.getString(APPLICATION_LOCALE_COUNTRY_KEY, "")!!, preferences.getString(APPLICATION_LOCALE_VARIANT_KEY, "")!! ) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 195072465b..ca42a07d50 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -26,6 +26,7 @@ import com.squareup.seismic.ShakeDetector import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.time.Clock import im.vector.app.features.disclaimer.SHARED_PREF_KEY import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.themes.ThemeUtils @@ -33,7 +34,10 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import javax.inject.Inject -class VectorPreferences @Inject constructor(private val context: Context) { +class VectorPreferences @Inject constructor( + private val context: Context, + private val clock: Clock, +) { companion object { const val SETTINGS_HELP_PREFERENCE_KEY = "SETTINGS_HELP_PREFERENCE_KEY" @@ -664,9 +668,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { */ fun getMinMediasLastAccessTime(): Long { return when (getSelectedMediasSavingPeriod()) { - MEDIA_SAVING_3_DAYS -> System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60 - MEDIA_SAVING_1_WEEK -> System.currentTimeMillis() / 1000 - 7 * 24 * 60 * 60 - MEDIA_SAVING_1_MONTH -> System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 + MEDIA_SAVING_3_DAYS -> clock.epochMillis() / 1000 - 3 * 24 * 60 * 60 + MEDIA_SAVING_1_WEEK -> clock.epochMillis() / 1000 - 7 * 24 * 60 * 60 + MEDIA_SAVING_1_MONTH -> clock.epochMillis() / 1000 - 30 * 24 * 60 * 60 MEDIA_SAVING_FOREVER -> 0 else -> 0 } @@ -872,8 +876,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { * @return true if user should always appear offline */ fun userAlwaysAppearsOffline(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE, - getDefault(R.bool.settings_presence_user_always_appears_offline_default)) + return defaultPrefs.getBoolean( + SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE, + getDefault(R.bool.settings_presence_user_always_appears_offline_default) + ) } /** @@ -1005,9 +1011,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun prefSpacesShowAllRoomInHome(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, + return defaultPrefs.getBoolean( + SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, // migration of old property - !labsSpacesOnlyOrphansInHome()) + !labsSpacesOnlyOrphansInHome() + ) } /* diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index 725c4ceabc..dddda261cd 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -77,10 +77,12 @@ class VectorSettingsActivity : VectorBaseActivity SettingsActivityPayload.SecurityPrivacy -> replaceFragment(views.vectorSettingsPage, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) SettingsActivityPayload.SecurityPrivacyManageSessions -> - replaceFragment(views.vectorSettingsPage, + replaceFragment( + views.vectorSettingsPage, VectorSettingsDevicesFragment::class.java, null, - FRAGMENT_TAG) + FRAGMENT_TAG + ) SettingsActivityPayload.Notifications -> { requestHighlightPreferenceKeyOnResume(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) replaceFragment(views.vectorSettingsPage, VectorSettingsNotificationPreferenceFragment::class.java, null, FRAGMENT_TAG) @@ -161,7 +163,8 @@ class VectorSettingsActivity : VectorBaseActivity } companion object { - fun getIntent(context: Context, directAccess: Int) = Companion.getIntent(context, when (directAccess) { + fun getIntent(context: Context, directAccess: Int) = Companion.getIntent( + context, when (directAccess) { EXTRA_DIRECT_ACCESS_ROOT -> SettingsActivityPayload.Root EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> SettingsActivityPayload.AdvancedSettings EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> SettingsActivityPayload.SecurityPrivacy @@ -173,7 +176,8 @@ class VectorSettingsActivity : VectorBaseActivity Timber.w("Unknown directAccess: $directAccess defaulting to Root") SettingsActivityPayload.Root } - }) + } + ) fun getIntent(context: Context, payload: SettingsActivityPayload) = Intent(context, VectorSettingsActivity::class.java) .applyPayload(payload) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index 678356b05b..99acea79df 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.getSizeOfFiles import im.vector.app.core.utils.toast @@ -74,7 +75,8 @@ import java.util.UUID import javax.inject.Inject class VectorSettingsGeneralFragment @Inject constructor( - colorProvider: ColorProvider + colorProvider: ColorProvider, + clock: Clock, ) : VectorSettingsBaseFragment(), GalleryOrCameraDialogHelper.Listener { @@ -82,7 +84,7 @@ class VectorSettingsGeneralFragment @Inject constructor( override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) private val mUserSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index 3a999f299f..5844467a1f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import androidx.preference.Preference import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.copyToClipboard @@ -72,7 +73,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( } it.setOnPreferenceClickListener { pref -> - copyToClipboard(requireContext(), pref.summary) + copyToClipboard(requireContext(), pref.summary.orEmpty()) true } } @@ -82,7 +83,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( it.summary = Matrix.getSdkVersion() it.setOnPreferenceClickListener { pref -> - copyToClipboard(requireContext(), pref.summary) + copyToClipboard(requireContext(), pref.summary.orEmpty()) true } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index af9ac52d1e..5033400425 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -147,8 +147,10 @@ class VectorSettingsPreferencesFragment @Inject constructor( it.onPreferenceClickListener = Preference.OnPreferenceClickListener { context?.let { context: Context -> MaterialAlertDialogBuilder(context) - .setSingleChoiceItems(R.array.media_saving_choice, - vectorPreferences.getSelectedMediasSavingPeriod()) { d, n -> + .setSingleChoiceItems( + R.array.media_saving_choice, + vectorPreferences.getSelectedMediasSavingPeriod() + ) { d, n -> vectorPreferences.setSelectedMediasSavingPeriod(n) d.cancel() diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 70ed3f441e..622f1f3f97 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -151,7 +151,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( findPreference("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!! } - override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView { + override fun onCreateRecyclerView(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): RecyclerView { return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also { // Insert animation are really annoying the first time the list is shown // due to the way preference fragment is done, it's not trivial to disable it for first appearance only.. @@ -275,13 +275,17 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( refreshXSigningStatus() secureBackupPreference.icon = activity?.let { - ThemeUtils.tintDrawable(it, - ContextCompat.getDrawable(it, R.drawable.ic_secure_backup)!!, R.attr.vctr_content_primary) + ThemeUtils.tintDrawable( + it, + ContextCompat.getDrawable(it, R.drawable.ic_secure_backup)!!, R.attr.vctr_content_primary + ) } ignoredUsersPreference.icon = activity?.let { - ThemeUtils.tintDrawable(it, - ContextCompat.getDrawable(it, R.drawable.ic_settings_root_ignored_users)!!, R.attr.vctr_content_primary) + ThemeUtils.tintDrawable( + it, + ContextCompat.getDrawable(it, R.drawable.ic_settings_root_ignored_users)!!, R.attr.vctr_content_primary + ) } findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.let { @@ -398,7 +402,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( navigator.openPinCode( requireContext(), pinActivityResultLauncher, - PinMode.AUTH) + PinMode.AUTH + ) } else { doOpenPinCodePreferenceScreen() } @@ -512,10 +517,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( if (data != null) { MaterialAlertDialogBuilder(thisActivity) - .setMessage(resources.getQuantityString(R.plurals.encryption_import_room_keys_success, - data.successfullyNumberOfImportedKeys, - data.successfullyNumberOfImportedKeys, - data.totalNumberOfKeys)) + .setMessage( + resources.getQuantityString( + R.plurals.encryption_import_room_keys_success, + data.successfullyNumberOfImportedKeys, + data.successfullyNumberOfImportedKeys, + data.totalNumberOfKeys + ) + ) .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } .show() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index ea182e7d6b..56c1fccb06 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -95,8 +95,10 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment { - ReAuthActivity.newIntent(requireContext(), + ReAuthActivity.newIntent( + requireContext(), it.registrationFlowResponse, it.lastErrorCode, getString(R.string.deactivate_account_title) diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 51ab422935..6df92a0e5c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -85,10 +85,12 @@ class CrossSigningSettingsFragment @Inject constructor( Unit } is CrossSigningSettingsViewEvents.RequestReAuth -> { - ReAuthActivity.newIntent(requireContext(), + ReAuthActivity.newIntent( + requireContext(), event.registrationFlowResponse, event.lastErrorCode, - getString(R.string.initialize_cross_signing)).let { intent -> + getString(R.string.initialize_cross_signing) + ).let { intent -> reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 94d6b8ff93..c81064c8d9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -110,7 +110,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( uiaContinuation = promise } } - }, it) + }, it + ) } } catch (failure: Throwable) { handleInitializeXSigningError(failure) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt index 8f04534440..648e9b3261 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt @@ -99,9 +99,10 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( style(ItemStyle.BIG_TEXT) titleIconResourceId(shield) title(host.stringProvider.getString(R.string.crosssigning_verify_this_session).toEpoxyCharSequence()) - description(host.stringProvider - .getString(if (data.hasOtherSessions) R.string.confirm_your_identity else R.string.confirm_your_identity_quad_s) - .toEpoxyCharSequence() + description( + host.stringProvider + .getString(if (data.hasOtherSessions) R.string.confirm_your_identity else R.string.confirm_your_identity_quad_s) + .toEpoxyCharSequence() ) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 4558c4bfb4..e56e08d19e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -244,10 +244,12 @@ class DevicesViewModel @AssistedInject constructor( val txID = session.cryptoService() .verificationService() .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null) - _viewEvents.post(DevicesViewEvents.ShowVerifyDevice( - session.myUserId, - txID - )) + _viewEvents.post( + DevicesViewEvents.ShowVerifyDevice( + session.myUserId, + txID + ) + ) } private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state -> @@ -274,7 +276,8 @@ class DevicesViewModel @AssistedInject constructor( session.cryptoService().setDeviceVerification( DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), action.cryptoDeviceInfo.userId, - action.cryptoDeviceInfo.deviceId) + action.cryptoDeviceInfo.deviceId + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index e897cdccac..6e6556caaa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -162,10 +162,12 @@ class VectorSettingsDevicesFragment @Inject constructor( * Launch the re auth activity to get credentials */ private fun askForReAuthentication(reAuthReq: DevicesViewEvents.RequestReAuth) { - ReAuthActivity.newIntent(requireContext(), + ReAuthActivity.newIntent( + requireContext(), reAuthReq.registrationFlowResponse, reAuthReq.lastErrorCode, - getString(R.string.devices_delete_dialog_title)).let { intent -> + getString(R.string.devices_delete_dialog_title) + ).let { intent -> reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index 83740c5018..ec4ef26001 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding -import org.billcarsonfr.jsonviewer.JSonViewerDialog -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import javax.inject.Inject class GossipingEventsPaperTrailFragment @Inject constructor( @@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor( super.onDestroyView() } - override fun didTap(event: Event) { - if (event.isEncrypted()) { - event.toClearContentStringWithIndent() - } else { - event.toContentStringWithIndent() - }?.let { - JSonViewerDialog.newInstance( - it, - -1, - createJSonViewerStyleProvider(colorProvider) - ).show(childFragmentManager, "JSON_VIEWER") - } + override fun didTap(event: AuditTrail) { +// if (event.isEncrypted()) { +// event.toClearContentStringWithIndent() +// } else { +// event.toContentStringWithIndent() +// }?.let { +// JSonViewerDialog.newInstance( +// it, +// -1, +// createJSonViewerStyleProvider(colorProvider) +// ).show(childFragmentManager, "JSON_VIEWER") +// } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index 30c2757eff..e7f09b09e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail data class GossipingEventsPaperTrailState( - val events: Async> = Uninitialized + val events: Async> = Uninitialized ) : MavericksState class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt index 30c2efc3ce..b8d82699e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt @@ -17,62 +17,43 @@ package im.vector.app.features.settings.devtools import im.vector.app.core.resources.DateProvider -import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.threeten.bp.format.DateTimeFormatter class GossipingEventsSerializer { private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") - fun serialize(eventList: List): String { + fun serialize(eventList: List): String { return buildString { - eventList.forEach { - val clearType = it.getClearType() - append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ") - when (clearType) { - EventType.ROOM_KEY_REQUEST -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("sessionId: ${content.body?.sessionId} ") - } - append("requestedBy: ${content?.requestingDeviceId}") + eventList.forEach { trail -> + val type = trail.type + val info = trail.info + append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ") + append("sessionId: ${info.sessionId} ") + when (type) { + TrailType.IncomingKeyRequest -> { + append("from:${info.userId}|${info.deviceId} - ") } - EventType.FORWARDED_ROOM_KEY -> { - val encryptedContent = it.content.toModel() - val content = it.getClearContent().toModel() - - append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}") - span("\nFrom Device (sender key):") { - textStyle = "bold" + TrailType.OutgoingKeyForward -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") } } - EventType.ROOM_KEY -> { - val content = it.getClearContent() - append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}") - } - EventType.SEND_SECRET -> { - val content = it.getClearContent().toModel() - append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}") - } - EventType.REQUEST_SECRET -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("secretName:${content.secretName} ") + TrailType.OutgoingKeyWithheld -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? WithheldInfo)?.let { + append("code: ${it.code} ") } - append("requestedBy:${content?.requestingDeviceId}") } - EventType.ENCRYPTED -> { - append("Failed to Decrypt") + TrailType.IncomingKeyForward -> { + append("from:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") + } } else -> { append("??") diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt index dd016c2bf5..6a45968916 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController -import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider @@ -26,137 +25,75 @@ import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import javax.inject.Inject class GossipingTrailPagedEpoxyController @Inject constructor( private val vectorDateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider -) : PagedListEpoxyController( +) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { interface InteractionListener { - fun didTap(event: Event) + fun didTap(event: AuditTrail) } var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> { val host = this val event = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { id(event.hashCode()) itemClickAction { host.interactionListener?.didTap(event) } - title( - if (event.isEncrypted()) { - "${event.getClearType()} [encrypted]" - } else { - event.type - }?.toEpoxyCharSequence() - ) + title(event.type.name.toEpoxyCharSequence()) description( span { +host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - span("\nfrom: ") { + span("\n${host.senderFromTo(event.type)}: ") { textStyle = "bold" } - +"${event.senderId}" + +"${event.info.userId}|${event.info.deviceId}" + span("\nroomId: ") { + textStyle = "bold" + } + +event.info.roomId + span("\nsessionId: ") { + textStyle = "bold" + } + +event.info.sessionId apply { - if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsessionId:") { + when (event.type) { + TrailType.OutgoingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { textStyle = "bold" } - +" ${content.body?.sessionId}" + +"${fInfo.chainIndex}" } - span("\nrequestedBy: ") { - textStyle = "bold" - } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - val encryptedContent = event.content.toModel() - val content = event.getClearContent().toModel() - if (event.mxDecryptionResult == null) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.sessionId}" - span("\nFrom Device (sender key):") { - textStyle = "bold" - } - +" ${encryptedContent?.senderKey}" - } else if (event.getClearType() == EventType.ROOM_KEY) { - // it's a bit of a fake event for trail reasons - val content = event.getClearContent() - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.get("session_id")}" - span("\nroomId:") { - textStyle = "bold" - } - +" ${content?.get("room_id")}" - span("\nTo :") { - textStyle = "bold" - } - +" ${content?.get("_dest") ?: "me"}" - } else if (event.getClearType() == EventType.SEND_SECRET) { - val content = event.getClearContent().toModel() - - span("\nrequestId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\nFrom Device:") { - textStyle = "bold" - } - +" ${event.mxDecryptionResult?.payload?.get("sender_device")}" - } else if (event.getClearType() == EventType.REQUEST_SECRET) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsecretName:") { + TrailType.OutgoingKeyWithheld -> { + val fInfo = event.info as WithheldInfo + span("\ncode: ") { textStyle = "bold" } - +" ${content.secretName}" + +"${fInfo.code}" } - span("\nrequestedBy: ") { - textStyle = "bold" + TrailType.IncomingKeyRequest -> { + // no additional info } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.ENCRYPTED) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) + TrailType.IncomingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { + textStyle = "bold" + } + +"${fInfo.chainIndex}" + } + TrailType.Unknown -> { } } } @@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor( ) } } + + private fun senderFromTo(type: TrailType): String { + return when (type) { + TrailType.OutgoingKeyWithheld, + TrailType.OutgoingKeyForward -> "to" + TrailType.IncomingKeyRequest, + TrailType.IncomingKeyForward -> "from" + TrailType.Unknown -> "" + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt index 4653f04f2c..09dd198bee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } span("${roomKeyRequest.userId}") + +"\n" +host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) span("\nsessionId:") { textStyle = "bold" @@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } +"${roomKeyRequest.deviceId}" - span("\nstate: ") { - textStyle = "bold" - } - +roomKeyRequest.state.name }.toEpoxyCharSequence() ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index a8045c07e3..e0a130a1e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest data class KeyRequestListViewState( val incomingRequests: Async> = Uninitialized, - val outgoingRoomKeyRequests: Async> = Uninitialized + val outgoingRoomKeyRequests: Async> = Uninitialized ) : MavericksState class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index cef68c01c1..4225cbdbec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -36,12 +36,15 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.safeOpenOutputStream import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.time.Clock import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { +class KeyRequestsFragment @Inject constructor( + private val clock: Clock, +) : VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDevtoolKeyrequestsBinding { return FragmentDevtoolKeyrequestsBinding.inflate(inflater, container, false) @@ -126,7 +129,7 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment( +class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { @@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> { val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt index a11bc9b0cf..8eccc8c593 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -57,10 +57,12 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() lifecycleScope.launch { val result = runCatching { - session.pushRuleService().updatePushRuleActions(ruleAndKind.kind, + session.pushRuleService().updatePushRuleActions( + ruleAndKind.kind, ruleAndKind.pushRule.ruleId, enabled, - newActions) + newActions + ) } if (!isAdded) { return@launch diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt index fcad0820cc..d0a1bff50c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt @@ -119,10 +119,12 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : val results = keywords.map { runCatching { withContext(Dispatchers.Default) { - session.pushRuleService().updatePushRuleActions(RuleKind.CONTENT, + session.pushRuleService().updatePushRuleActions( + RuleKind.CONTENT, it, enabled, - newActions) + newActions + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index d9cd5b3461..d7c18b9c53 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -204,7 +204,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( // Important, Battery optim white listing is needed in this mode; // Even if using foreground service with foreground notif, it stops to work // in doze mode for certain devices :/ - if (!isIgnoringBatteryOptimizations(requireContext())) { + if (!requireContext().isIgnoringBatteryOptimizations()) { requestDisablingBatteryOptimization(requireActivity(), batteryStartForActivityResult) } } @@ -352,8 +352,8 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( super.onDetach() } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { - return when (preference?.key) { + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> { updateEnabledForAccount(preference) true @@ -373,9 +373,11 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( // Trick, we must enable this room to disable notifications lifecycleScope.launch { try { - pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, + pushRuleService.updatePushRuleEnableStatus( + RuleKind.OVERRIDE, it, - !switchPref.isChecked) + !switchPref.isChecked + ) // Push rules will be updated from the sync } catch (failure: Throwable) { if (!isAdded) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt index 1381dd79ae..7f856298ea 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt @@ -58,10 +58,12 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment : lifecycleScope.launch { val result = runCatching { - session.pushRuleService().updatePushRuleActions(kind, + session.pushRuleService().updatePushRuleActions( + kind, ruleId, enabled, - newActions) + newActions + ) } hideLoadingView() if (!isAdded) { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt index 172764d87f..d56562a7dd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt @@ -72,8 +72,10 @@ abstract class PushRuleItem : EpoxyModelWithHolder() { val description = StringBuffer() pushRule.conditions?.forEachIndexed { i, condition -> if (i > 0) description.append("\n") - description.append(condition.asExecutableCondition(pushRule)?.technicalDescription() - ?: "UNSUPPORTED") + description.append( + condition.asExecutableCondition(pushRule)?.technicalDescription() + ?: "UNSUPPORTED" + ) } if (description.isBlank()) { holder.description.text = "No Conditions" diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index 2a9db78121..4a3f895c51 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -68,10 +68,12 @@ class ThreePidsSettingsFragment @Inject constructor( } private fun askAuthentication(event: ThreePidsSettingsViewEvents.RequestReAuth) { - ReAuthActivity.newIntent(requireContext(), + ReAuthActivity.newIntent( + requireContext(), event.registrationFlowResponse, event.lastErrorCode, - getString(R.string.settings_add_email_address)).let { intent -> + getString(R.string.settings_add_email_address) + ).let { intent -> reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index 587337d210..6f26dd2b15 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -210,12 +210,18 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( withState { state -> val allThreePids = state.threePids.invoke().orEmpty() + state.pendingThreePids.invoke().orEmpty() if (allThreePids.any { it.value == action.threePid.value }) { - _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalArgumentException(stringProvider.getString( - when (action.threePid) { - is ThreePid.Email -> R.string.auth_email_already_defined - is ThreePid.Msisdn -> R.string.auth_msisdn_already_defined - } - )))) + _viewEvents.post( + ThreePidsSettingsViewEvents.Failure( + IllegalArgumentException( + stringProvider.getString( + when (action.threePid) { + is ThreePid.Email -> R.string.auth_email_already_defined + is ThreePid.Msisdn -> R.string.auth_msisdn_already_defined + } + ) + ) + ) + ) } else { viewModelScope.launch { loadingSuspendable { diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt index 69e3021738..ae57babf9a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt @@ -30,10 +30,12 @@ class TestPushRulesSettings @Inject constructor(private val activeSessionHolder: TroubleshootTest(R.string.settings_troubleshoot_test_bing_settings_title) { private val testedRules = - listOf(RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, + listOf( + RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, RuleIds.RULE_ID_CONTAIN_USER_NAME, RuleIds.RULE_ID_ONE_TO_ONE_ROOM, - RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS) + RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS + ) override fun perform(activityResultLauncher: ActivityResultLauncher) { val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt index 9acc81d0c2..d0d5bea536 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt @@ -66,10 +66,12 @@ class SoftLogoutActivity : LoginActivity() { supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) // And inform the user - showError(getString( - R.string.soft_logout_sso_not_same_user_error, - softLogoutViewEvents.currentUserId, - softLogoutViewEvents.newUserId) + showError( + getString( + R.string.soft_logout_sso_not_same_user_error, + softLogoutViewEvents.currentUserId, + softLogoutViewEvents.newUserId + ) ) } is SoftLogoutViewEvents.ClearData -> { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index e2f3c14e7d..daca308ac1 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -74,10 +74,14 @@ class SoftLogoutController @Inject constructor( } loginTextItem { id("signText1") - text(host.stringProvider.getString(R.string.soft_logout_signin_notice, - state.homeServerUrl.toReducedUrl(), - state.userDisplayName, - state.userId)) + text( + host.stringProvider.getString( + R.string.soft_logout_signin_notice, + state.homeServerUrl.toReducedUrl(), + state.userDisplayName, + state.userId + ) + ) } if (state.hasUnsavedKeys) { loginTextItem { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 8a682b4b5e..ea08640ab7 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -59,26 +59,32 @@ class SoftLogoutFragment @Inject constructor( softLogoutController.update(softLogoutViewState) when (val mode = softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { is LoginMode.SsoAndPassword -> { - loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( - softLogoutViewState.homeServerUrl, - softLogoutViewState.deviceId, - mode.ssoIdentityProviders - )) + loginViewModel.handle( + LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.ssoIdentityProviders + ) + ) } is LoginMode.Sso -> { - loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( - softLogoutViewState.homeServerUrl, - softLogoutViewState.deviceId, - mode.ssoIdentityProviders - )) + loginViewModel.handle( + LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.ssoIdentityProviders + ) + ) } LoginMode.Unsupported -> { // Prepare the loginViewModel for a SSO/login fallback recovery - loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( - softLogoutViewState.homeServerUrl, - softLogoutViewState.deviceId, - null - )) + loginViewModel.handle( + LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + null + ) + ) } else -> Unit } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 5f31e6b508..1d67f1ec98 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -159,9 +159,12 @@ class SoftLogoutViewModel @AssistedInject constructor( withState { softLogoutViewState -> if (softLogoutViewState.userId != action.credentials.userId) { Timber.w("User login again with SSO, but using another account") - _viewEvents.post(SoftLogoutViewEvents.ErrorNotSameUser( - softLogoutViewState.userId, - action.credentials.userId)) + _viewEvents.post( + SoftLogoutViewEvents.ErrorNotSameUser( + softLogoutViewState.userId, + action.credentials.userId + ) + ) } else { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index e8f3702efc..033e2ed667 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -123,7 +123,8 @@ class SpaceCreationActivity : SimpleFragmentActivity() { val frag = supportFragmentManager.findFragmentByTag(fragmentClass.name) ?: createFragment(fragmentClass) supportFragmentManager.beginTransaction() .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) - .replace(views.container.id, + .replace( + views.container.id, frag, fragmentClass.name ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt index 680124e091..a46d38349d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt @@ -104,11 +104,13 @@ class SpaceExploreActivity : VectorBaseActivity(), Matrix MatrixToBottomSheet.withLink(it.link, OriginOfMatrixTo.SPACE_EXPLORE).show(supportFragmentManager, "ShowChild") } is SpaceDirectoryViewEvents.NavigateToCreateNewRoom -> { - createRoomResultLauncher.launch(CreateRoomActivity.getIntent( - this, - openAfterCreate = false, - currentSpaceId = it.currentSpaceId - )) + createRoomResultLauncher.launch( + CreateRoomActivity.getIntent( + this, + openAfterCreate = false, + currentSpaceId = it.currentSpaceId + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 6cffabd851..cc75fd5b2e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -65,7 +65,8 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { if (hasChildren) { holder.collapseIndicator.isVisible = true holder.collapseIndicator.setImageDrawable( - ContextCompat.getDrawable(holder.view.context, + ContextCompat.getDrawable( + holder.view.context, if (expanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more ) ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt index f50d418de3..201282b113 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt @@ -57,7 +57,8 @@ abstract class SubSpaceSummaryItem : VectorEpoxyModel(), SpaceDetailEpoxyController.Listener, GalleryOrCameraDialogHelper.Listener, OnBackPressed { @@ -42,7 +44,7 @@ class CreateSpaceDetailsFragment @Inject constructor( override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index b25ae4f2c7..aee405388b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -102,7 +102,8 @@ class SpaceDetailEpoxyController @Inject constructor( } errorMessage( host.roomAliasErrorFormatter.format( - (((data.aliasVerificationTask as? Fail)?.error) as? RoomAliasError)) + (((data.aliasVerificationTask as? Fail)?.error) as? RoomAliasError) + ) ) onTextChange(host.aliasTextWatcher) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 5993dd2c81..2ddcb42e2a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -111,8 +111,12 @@ class SpaceDirectoryViewModel @AssistedInject constructor( .onEach { val powerLevelsHelper = PowerLevelsHelper(it) setState { - copy(canAddRooms = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_SPACE_CHILD)) + copy( + canAddRooms = powerLevelsHelper.isUserAllowedToSend( + session.myUserId, true, + EventType.STATE_SPACE_CHILD + ) + ) } } .launchIn(viewModelScope) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 57b1c97efb..0264fdabfc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -38,7 +38,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider +import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.home.AvatarRenderer @@ -60,9 +60,9 @@ import javax.inject.Inject class SpaceSettingsFragment @Inject constructor( private val epoxyController: SpaceSettingsController, - private val colorProvider: ColorProvider, + colorProvider: ColorProvider, + clock: Clock, private val avatarRenderer: AvatarRenderer, - private val drawableProvider: DrawableProvider ) : VectorBaseFragment(), SpaceSettingsController.Callback, GalleryOrCameraDialogHelper.Listener, @@ -73,7 +73,7 @@ class SpaceSettingsFragment @Inject constructor( private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel - private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider, clock) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentRoomSettingGenericBinding.inflate(inflater) @@ -236,7 +236,8 @@ class SpaceSettingsFragment @Inject constructor( RoomSettingsAction.SetAvatarAction( RoomSettingsViewState.AvatarAction.UpdateAvatar( newAvatarUri = uri, - newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) ) ) } diff --git a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt index 01e933e446..27116093d2 100755 --- a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt @@ -54,7 +54,7 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute views.syncStateProgressBar.isVisible = newState is SyncState.Running && newState.afterPause if (newState == SyncState.NoNetwork) { - val isAirplaneModeOn = isAirplaneModeOn(context) + val isAirplaneModeOn = context.isAirplaneModeOn() views.syncStateNoNetwork.isVisible = isAirplaneModeOn.not() views.syncStateNoNetworkAirplane.isVisible = isAirplaneModeOn } else { diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index 3a9b772630..28febdac37 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -111,7 +111,8 @@ class ReviewTermsViewModel @AssistedInject constructor( try { val data = session.termsService().getTerms(termsArgs.type, termsArgs.baseURL) val terms = data.serverResponse.getLocalizedTerms(action.preferredLanguageCode).map { - Term(it.localizedUrl ?: "", + Term( + it.localizedUrl ?: "", it.localizedName ?: "", it.version, accepted = data.alreadyAcceptedTermUrls.contains(it.localizedUrl) diff --git a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt index e46c3516ca..6693d7436c 100644 --- a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt @@ -50,12 +50,14 @@ class SharedPreferencesUiStateRepository @Inject constructor( override fun storeDisplayMode(displayMode: RoomListDisplayMode) { sharedPreferences.edit { - putInt(KEY_DISPLAY_MODE, + putInt( + KEY_DISPLAY_MODE, when (displayMode) { RoomListDisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE RoomListDisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS else -> VALUE_DISPLAY_MODE_CATCHUP - }) + } + ) } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 02955cf2b3..505bbdd2dd 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -75,11 +75,13 @@ class UserCodeSharedViewModel @AssistedInject constructor( private fun handleShareByText() { session.permalinkService().createPermalink(session.myUserId)?.let { permalink -> val text = stringProvider.getString(R.string.invite_friends_text, permalink) - _viewEvents.post(UserCodeShareViewEvents.SharePlainText( - text, - stringProvider.getString(R.string.invite_friends), - stringProvider.getString(R.string.invite_friends_rich_title) - )) + _viewEvents.post( + UserCodeShareViewEvents.SharePlainText( + text, + stringProvider.getString(R.string.invite_friends), + stringProvider.getString(R.string.invite_friends_rich_title) + ) + ) } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 291153ef2b..bb1f8db6ff 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem +import kotlin.random.Random data class ThreePidUser( val email: String, @@ -142,7 +143,7 @@ class UserListViewModel @AssistedInject constructor( } private fun retryUserSearch(state: UserListViewState) { - identityServerUsersSearch.tryEmit(UserSearch(state.searchTerm, cacheBuster = System.currentTimeMillis())) + identityServerUsersSearch.tryEmit(UserSearch(state.searchTerm, cacheBuster = Random.nextLong())) } private fun handleSearchUsers(searchTerm: String) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt index d2f7927d75..6c9b8d34c2 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt @@ -20,11 +20,13 @@ import android.content.Context import android.os.Build import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode +import im.vector.app.core.time.Clock import timber.log.Timber import java.io.File import javax.inject.Inject class VoicePlayerHelper @Inject constructor( + private val clock: Clock, context: Context ) { private val outputDirectory: File by lazy { @@ -46,9 +48,9 @@ class VoicePlayerHelper @Inject constructor( if (targetFile.exists()) { targetFile.delete() } - val start = System.currentTimeMillis() + val start = clock.epochMillis() val session = FFmpegKit.execute("-i \"${file.path}\" -c:a aac \"${targetFile.path}\"") - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.d("Convert to mp4 in $duration ms. Size in bytes from ${file.length()} to ${targetFile.length()}") return when { ReturnCode.isSuccess(session.returnCode) -> { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index 2d40f5f7a3..e5fd60dbc3 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -23,10 +23,14 @@ import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.Level import com.arthenica.ffmpegkit.ReturnCode import im.vector.app.BuildConfig +import im.vector.app.core.time.Clock import timber.log.Timber import java.io.File -class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") { +class VoiceRecorderL( + context: Context, + private val clock: Clock, +) : AbstractVoiceRecorder(context, "mp4") { override fun setOutputFormat(mediaRecorder: MediaRecorder) { // Use AAC/MP4 format here mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) @@ -43,9 +47,9 @@ class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") { if (targetFile.exists()) { targetFile.delete() } - val start = System.currentTimeMillis() + val start = clock.epochMillis() val session = FFmpegKit.execute("-i \"${recordedFile.path}\" -c:a libvorbis \"${targetFile.path}\"") - val duration = System.currentTimeMillis() - start + val duration = clock.epochMillis() - start Timber.d("Convert to ogg in $duration ms. Size in bytes from ${recordedFile.length()} to ${targetFile.length()}") return when { ReturnCode.isSuccess(session.returnCode) -> { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt index 004d520a6f..0cee8f4f6e 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -18,16 +18,18 @@ package im.vector.app.features.voice import android.content.Context import android.os.Build +import im.vector.app.core.time.Clock import javax.inject.Inject class VoiceRecorderProvider @Inject constructor( - private val context: Context + private val context: Context, + private val clock: Clock, ) { fun provideVoiceRecorder(): VoiceRecorder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { VoiceRecorderQ(context) } else { - VoiceRecorderL(context) + VoiceRecorderL(context, clock) } } } diff --git a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewClient.kt b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewClient.kt index 597486491c..264c788315 100644 --- a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewClient.kt +++ b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewClient.kt @@ -59,9 +59,11 @@ class VectorWebViewClient(private val eventListener: WebViewEventListener) : Web override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { super.onReceivedHttpError(view, request, errorResponse) - eventListener.onHttpError(request.url.toString(), + eventListener.onHttpError( + request.url.toString(), errorResponse.statusCode, - errorResponse.reasonPhrase) + errorResponse.reasonPhrase + ) } override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index dbd63186b6..cd2a4dcdf4 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -156,7 +156,8 @@ class WidgetFragment @Inject constructor() : integrationManagerActivityResultLauncher, state.roomId, state.widgetId, - state.widgetKind.screenId) + state.widgetKind.screenId + ) return@withState true } R.id.action_delete -> { diff --git a/vector/src/main/res/drawable/ic_settings_root_flair.xml b/vector/src/main/res/drawable/ic_settings_root_flair.xml deleted file mode 100644 index b074ae478e..0000000000 --- a/vector/src/main/res/drawable/ic_settings_root_flair.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cfe52..6f3b755bf5 100644 --- a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cfe52..6f3b755bf5 100644 --- a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml index fe3abdcf80..1e619580f2 100644 --- a/vector/src/main/res/values-ar/strings.xml +++ b/vector/src/main/res/values-ar/strings.xml @@ -245,7 +245,7 @@ أزِل انضم ارفض - انتقل إلى أول رسالة غير مقروءة. + انتقل إلى غير المقروءة. ترك الغرفة أمتأكّد من ترك الغرفة؟ امنع @@ -253,7 +253,7 @@ أخفِ كل رسائل هذا المستخدم اعرض كل رسائل هذا المستخدم ‏⁨%1$s⁩ يكتب… - لا تصريح لديك للنشر في هذه الغرفة + لا أذن لديك للنشر في هذه الغرفة. اخرج البصمة (⁨%s⁩): تعذّر التحقق من معرّف الخادوم البعيد. @@ -408,7 +408,7 @@ أشِر إليه لن تستطيع العودة عن هذا التغيير إذ أنك تمنح المستخدم نفس مستوى السلطة الذي لك. أمتأكد؟ - أمتأكد من منع هذا المستخدم من هذه الدردشة؟ + حظر مستخدم عن هذه الغرفة سيمنعه من الانضمام إليها ثانية. ثِق لا تثق تجاهل @@ -481,7 +481,7 @@ <b>غير<b/> مؤكّدة مؤكّدة أكّد - اسرد الأعضاء + أعضاء ادعُ قد يعني هذا بأن أحدهم يعترض الاتصال بعدوانية، أو أن هاتفك لا يثق بالشهادة التي قدّمها الخادوم البعيد. إن قال مدير الخادوم بأن هذا متوقع، فتأكد من أن البصمة أدناه تطابق البصمة التي وفّرها. @@ -735,7 +735,7 @@ %d مكالمةً فائة %d مكالمة فائة - يتصل… + يَرِن… اختر نغمة للمكالمات: نغمة المكالمات الواردة أكّد المكالمة قبل إجرائها @@ -1001,4 +1001,142 @@ غادر غادر الغرفة المرفوعات + أنت تستعرض هذا النقاش سلفًا! + أنت تستعرض هذه الغرفة سلفًا! + منفصل عن الشبكة. تحقق من اتصالك. + حدث عالجه مدير الغرفة. + ستعرض غرفك هنا. لانضمام لغرفة أو لإنشاء واحدة اضغط زر +. + ستعرض رسائلك المباشرة هنا. لبدأ محادثات جديدة اضغط زر +. + المحادثات + الغُرف + جميع الريائل مقروءة + دعاه %s + أرسلَ لك طلبًا + أعد المحاولة + اعرضه في الغرفة + ردّ + حرّر + خطأ مجهول + يريد %s التحقق من جلستك + طلب تحقق + فهمتُ + مستوثق! + التوقيع + الخوارزمية + النسخة + يتحقق من حالة النسخ الاحتياطي + استعد من نسخ احتياطي + الأذونات + أذونات الفضاء + أذونات الغرفة + %1$s و%2$s وآخرون + %1$s و%2$s + فك الحظر عن مستخدم سيمح له الانضمام لهذا الفضاء. + فك الحظر عن مستخدم سيمح له الانضمام لهذه الغرفة. + حظر مستخدم عن هذه الفضاء سيمنعه من الانضمام إليه ثانية. + فك حظر مستخدم + سبب الحظر + احظر مستخدمًا + سيزال المستخدم من هذه الفضاء. +\n +\nلمنعه من الانضمام ثانية لهذه الفضاء احظره. + سيزال المستخدم من هذه الغرفة. +\n +\nلمنعه من الانضمام ثانية لهذه الغرفة احظره. + سبب الإزالة + أزل مستخدمًا + أمتأكد من سحب دعوة هذا المستخدم؟ + اسحب الدعوة + ألغ تجاهل المستخدم + بتجاهل هذا المستخدم ستزال الرسائل من الغرف المشتركة بينكما. +\n +\nيمكنم التراجع عن هذا الإجراء في أي وقت عبر الإعدادات العامة. + تجاهل مستخدم + اخفض الرتبة + لن تتمكن من التراجع عن هذا التغيير لأنك ستخفض رتبتك ، إذا كنت آخر مستخدم ذي امتياز في الغرفة ، فسيكون من المستحيل استعادة الامتيازات. + أتريد خفض رتبتك؟ + اسحب الدعوة + هذه الغرفة ليست علنية، لا يمكنك إعادة الانضمام إليها بدون دعوة. + امنح اذن النفاذ للمتراسلين. + لمسح رمز الاستحابة السريعة اسمح بالنفاذ للكاميرا. + تجري مكالمة مرئية… + + لا مكالمات مرئية فائتة + مكالمة مرئية فائتة + مكالمتان مرئيتان فائتتان + %d مكالمات مرئية فائتة + %d مكالمة مرئية فائتة + %d مكالمة مرئية فائتة + + استخدم نغمة ${app_name} الافتراضية للمكالمات الواردة + امنع المكالمات العَرضية + استزد + عدّل + جربه + ليس الآن + عطّل + فعّل + لا يمكنك إجراء مكالمة مع نفسك، انتظر قبول منتسبين لطلب + شغّلتّ التعمية بين الطرفيات (خورزمية مجهولة %1$s). + وضع المزامنة في الخلفية + على الهاتف لن تتلق إخطارات عند ذكرك أو ذكر كلمة مفتاحية في الغرف المعماة. + ترقيات الغرف + رسائل آلي (Bot) + الكلمات المفتاحيّة + \@room + الرسائل المباشرة المعماة + الرسائل المباشرة + اسم المستخدم + اسمي العلني + عند ترقية الغرف + الرسائل المعماة في المحادثات الجماعية + الرسائل المعماة في المحادثات الفردية + اختر لون ضوء التنبيهوالاهتزاز والصوت… + اضبط الإخطارات الصامتة + اضبط إخطارات المكالمات + تجاهل التحسين + يأثر تحسين البطارية على عمل ${app_name}. + تحسين البطارية + عطّل القيود + ابدأ مع التشغيل + فعّل البدأ مع التشغيل + ستبدأ الخدمة عند إعادة تشغيل الجهاز. + نُقر الإخطار! + رجاء أنقر على الإخطارات، إن لم تستطع رؤيتها تحقق من إعدادات النظام. + عرض الإخطارات + أنت تستعرض الإخطارات! أنقرني! + أضف حسابًا + يمكن للمدعووين فقط العثور على الغرفة والانضمام إليها + إعداد نفاذ مجهول (%s) + يمكن لأي شخص طلب الانضمام لهذه الغرفة ليحدد الأعضاء إن كانوا سيقبلونهم أو يرفضونهم + أتريد نشر هذه الغرفة للعلن في دليل %1$s؟ + ألغ نشر هذا العنوان + انشر هذا العنوان + أضف عنوانًا محليًا + لا تملك هذه الغرفة عناوين محلية + العناوين المحلية + أتريد حذف العنوان \"%1$s\" ؟ + أتريد إلغاء تشر العنوان \"%1$s\" ؟ + انشر + الحظور + اختر + المصدر الافتراضي للوسائط + اختر + الوسائط + أدر البريد الإلكتروني وأرقام الهاتف المرتبطة بحسابك + البريد الإلكتروني وأرقام الهاتف + كلمة السر غير صالحة + كلمة السر + لا ودجات نشطة + استخدم المايكروفون + استخدم الكاميرا + احجب الكل + اسمح + معرف الغرفة + معرف الودجة + سمتك + معرفك + رابط صورتك الرمزية + اسمك العلني + افتح في المتصفح \ No newline at end of file diff --git a/vector/src/main/res/values-bn/strings.xml b/vector/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..8555330f05 --- /dev/null +++ b/vector/src/main/res/values-bn/strings.xml @@ -0,0 +1,4 @@ + + + %s এর আমন্ত্রণ + \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 3121f859f3..61be21b7bf 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -1566,7 +1566,9 @@ \n \nMůžete tuto akci kdykoli zvrátit v obecných nastaveních. Odignorovat uživatele - Odignorování tohoto uživatele opět ukáže všechny jejich zprávy. + Zrušení ignorování tohoto uživatele opět ukáže všechny jejich zprávy. +\n +\nTato akce povede k restartování aplikace a může trvat nějakou dobu. Zrušit pozvánku Jste si jisti, že chcete zrušit pozvánku tomuto uživateli\? Vykopnout uživatele @@ -2504,4 +2506,32 @@ Vezmi mě domů Přizpůsobit profil Zakázat + Načítání polohy živě… + 8 hodin + 1 hodinu + 15 minut + Sdílet svoji polohu živě na + (%1$s) + %1$s (%2$s) + Nelze přehrát %1$s + Pozastavit %1$s + Přehrát %1$s + %1$d minut %2$d sekund + %1$s, %2$s, %3$s + Sdíleli svou polohu živě + ${app_name} se skvěle hodí i na pracoviště. Aplikaci důvěřují nejlépe zabezpečené organizace na světě. + BETA + Vlákna jsou ve fázi vývoje s novými zajímavými funkcemi, jako jsou například vylepšená oznámení. Rádi si vyslechneme vaši zpětnou vazbu! + Zpětná vazba k vláknům Beta + Poskytnout zpětnou vazbu + BETA + Pokud je tato funkce povolena, budete se ostatním uživatelům vždy jevit jako offline, a to i při používání aplikace. + Režim offline + Přítomnost + Váš domovský server v současné době vlákna nepodporuje, takže tato funkce může být nespolehlivá. Některé zprávy ve vláknech nemusí být spolehlivě dostupné. %sChcete přesto vlákna povolit\? + Vlákna Beta + Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat. %sPovolením vláken se aplikace restartuje. U některých účtů to může trvat déle. + Vlákna Beta + Zjistit více + Vyzkoušet to \ No newline at end of file diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 0f80bc8e0f..a4ab637dae 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1,5 +1,5 @@ - + la invitación de %s %1$s invitó a %2$s %1$s te ha invitado @@ -317,7 +317,7 @@ Eliminar Unirse Rechazar - Mensajes no leídos. + Ir a mensajes no leídos Salir de la sala ¿Seguro que quieres salir de la sala? CONVERSACIONES DIRECTAS @@ -332,7 +332,7 @@ %s está escribiendo… %1$s y %2$s están escribiendo… %1$s y %2$s y otros están escribiendo… - No tienes permiso para publicar en esta sala + No tienes permiso para publicar en esta sala. Confiar No confiar Cerrar Sesión @@ -574,7 +574,7 @@ Quita el veto al usuario con la ID dada Define el nivel de autoridad de un usuario Invita al usuario con la ID dada a la sala actual - Se une a la sala con el alias dado + Se une a la sala con la dirección dada Salir de la sala Establecer el tema de la sala Expulsa al usuario con la ID dada @@ -582,7 +582,7 @@ Activar/Desactivar markdown Para reparar la gestión de las Aplicaciones de Matrix Vista previa de medios antes de enviar - Esta sala ha sido reemplazada y ya no está activa + Esta sala ha sido reemplazada y ya no está activa. La conversación continúa aquí Esta sala es una continuación de otra conversación Haz clic aquí para ver mensajes más antiguos @@ -972,7 +972,7 @@ Enviar un nuevo mensaje directo Ver el directorio de la sala Nombre o ID (#ejemplo:servidor.org) - Habilitar \"desplazar para contestar\" en la línea de tiempo + Habilitar \"deslizar para contestar\" en la línea de tiempo Enlace copiado al portapapeles Creando sala… Ver historial de modificaciones @@ -1306,7 +1306,9 @@ \n \n Puede revertir esta acción en cualquier momento en la configuración general. Dejar de ignorar al usuario - Si ignora a este usuario, se mostrarán todos sus mensajes nuevamente. + Si deja de ignorar a este usuario, se mostrarán todos sus mensajes nuevamente. +\n +\nTome nota de que esta acción reiniciará la aplicación y podría tardar algún tiempo. ¿Estás seguro de que deseas cancelar la invitación de este usuario\? Patear usuario patear al usuario los eliminará de esta sala. @@ -1482,7 +1484,7 @@ Una vez habilitado, el cifrado de una sala no se puede deshabilitar. Los mensajes enviados en una sala cifrada no pueden ser vistos por el servidor, solo por los participantes de la sala. Habilitar el cifrado puede impedir que muchos bots y puentes funcionen correctamente. Para estar seguro, verifique %s comprobando un código de un solo uso. Para estar seguro, hágalo en persona o use otra forma de comunicarse. - Compare los emoji únicos, asegurándose de que aparezcan en el mismo orden. + Verifica si los mismos emojis aparecen en el mismo orden en ambos usuarios. Compare el código con el que se muestra en la pantalla del otro usuario. Los mensajes con este usuario están cifrados Extremo-a-Extremo y no pueden ser leídos por terceros. Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza. @@ -1505,7 +1507,7 @@ Esta sesión es de confianza para mensajería segura porque %1$s (%2$s) la ha verificado: %1$s (%2$s) iniciado sesión con una nueva sesión: Hasta que este usuario confíe en esta sesión, los mensajes enviados hacia y desde ella se etiquetan con advertencias. Alternativamente, puede verificarlo manualmente. - ¡Casi ahí! ¿Es %s muestra el mismo escudo\? + ¡Casi estamos! ¿Muestra %s el mismo tick\? Use una contraseña o clave de recuperación Si no puede acceder a una sesión existente No puedo encontrar secretos almacenados @@ -1560,7 +1562,7 @@ Cifrado no habilitado El cifrado usado por esta sala no es compatible %s creado y configurado la sala. - ¡Casi ahí! ¿El otro dispositivo muestra el mismo escudo\? + ¡Casi estamos! ¿El otro dispositivo muestra el mismo tick\? ¡Casi ahí! Esperando confirmación… No se pudieron importar las claves Mensajes que contienen @room @@ -1822,8 +1824,8 @@ Continuar Los espacios son una nueva manera para agrupar salas y personas. Únete a un Espacio - Crea un Espacio - Añade un Espacio + Crear espacio + Añadir espacio Crea un Espacio Crear un espacio Sincronización inicial: @@ -1991,7 +1993,7 @@ Solo la gente invitada puede encontrar y unirse a l sala Privada (solo mediante invitación) Cualquiera puede solicitar entrar en la sala, serán los miembros quienes decidan aceptar o rechazar la solicitud - Configurar las direcciones para esta sala de forma que otros usuarios puedan encontrarla mediante tu homeserver (%1$s). + Configurar las direcciones para esta sala de forma que otros usuarios puedan encontrarla mediante tu servidor doméstico (%1$s) Ninguna dirección publicada aún, añade una debajo. Las direcciones publicadas pueden ser usadas por cualquiera en cualquier servidor para unirse a tu sala. Para publicar una dirección es necesario configurarla previamente como dirección local. Ver y gestionar direcciones de este espacio. @@ -2018,7 +2020,7 @@ Llamada de audio perdida %d llamadas de audio perdidas - Homeserver API URL + URL de API del servidor doméstico Faltan permisos Para enviar mensajes de voz has de otorgar el permiso de Micrófono. Para llevar a cabo esta acción has de otorgar el permiso de Cámara en las preferencias del sistema. @@ -2036,7 +2038,7 @@ Otros espacios o habitaciones que puede que no conozcas Unirse a la sala de repuesto Tiene borradores sin enviar - Puedes contactarme si tienes dudas de seguimiento + Puedes contactarme si tienes alguna duda Necesitas permiso para actualizar una sala Actualizar el espacio padre automáticamente Invitar usuarios automáticamente @@ -2077,7 +2079,7 @@ Añade algunos detalles para ayudarlo a destacar. Puedes cambiar esto en cualquier momento. Un espacio privado para ti y tus compañeros de equipo Un espacio privado para organizar tus salas - Asegúrate de que son las personas correctas las que tienen acceso a %s. Puedes cambiar esto más tarde. + Asegúrate de que las personas correctas tienen acceso a %s. ¿Con quién estás trabajando\? Para unirte a un espacio existente necesitas una invitación. Puedes cambiar esto más tarde @@ -2178,7 +2180,7 @@ Cambiar el avatar de la sala actual Cambiar su apodo para mostrar solo en la sala actual Establecer el nombre de la sala - Deja de ignorar a un usuario, muestra sus mensajes en el futuro. + Deja de ignorar a un usuario, muestra sus mensajes en el futuro Ignorar a un usuario, ocultándole sus mensajes Espacio que sabes que contiene esta sala Cualquiera puede encontrar el espacio y unirse @@ -2304,4 +2306,181 @@ Filtrar %1$s, %2$s y otros Copiar enlace al hilo + Enviar sticker + Ocurrió un error al cargar el mapa + ¡Estás listo! + Desde un hilo + Los hilos ayudan a mantener sus conversaciones en el tema fijado y son fáciles de organizar. + Puedes apagar esto en cualquier momento en configuración + Tu servidor doméstico actualmente no soporta hilos, por lo tanto esta función podría ser poco fiable. Algunos mensajes en hilos puede que no estén disponibles de forma fiable. %s ¿Deseas activar la función de hilos de todas formas\? + Beta de hilos + Nos estamos acercando a el lanzamiento de una beta pública para hilos. +\n +\nMientras nos preparamos, tenemos que hacer algunos cambios: los hilos creados a partir de ahora se mostrarán como respuestas normales. +\n +\nEsto será una transición debido a que los hilos son ahora parte de la especificación Matrix. + Hilos acercándose a versión beta 🎉 + Ya tengo una cuenta + Crear cuenta + Saltar este paso + Guardar y continuar + Tus preferencias han sido guardadas. + Vámonos + Añadir una foto de perfil + Puedes cambiar esto más tarde + Nombre público + Escoger un nombre público + Tu cuenta %s ha sido creada. + ¡Felicidades! + Dar opinión + BETA + Modo fuera de línea + Presencia + Nosotros no compartimos información con terceros + Nosotros no grabamos o perfilamos ningún dato de cuenta + Ayudar a mejorar ${app_name} + 8 horas + 1 hora + 15 minutos + Permitir acceso + Mostrar ubicaciónes de usuarios en la línea de tiempo + Ubicación en vivo activada + Cargando ubicación en vivo… + Parar + Mostrar burbujas de mensajes + Abrir cámara + Enviar imágenes y vídeos + Subir archivo + Abrir contactos + + %1$d más + %1$d más + + Mostrar menos + Compartir ubicación + Crear encuesta + aquí + Aprender más + Pruébalo + Deshabilitar + + %d cambio de ACL del servidor + %d cambios de ALC de los servidores + + Compartir esta ubicación + Compartir ubicación en tiempo real + Compartir mi ubicación actual + Compartir ubicación + ${app_name} también es estupenda para el trabajo. Es confiada por las organizaciones más seguras del mundo. + Para descubrir contactos ya existentes, tendrás que enviar información de contacto (emails y números de teléfono) a tu servidor de identidad. Aplicamos hashes en tus datos antes de enviarlos por privacidad. + Los hilos son un trabajo en progreso con nuevas y excitantes características nuevas, como notificaciones mejoradas. ¡Nos encantaría escuchar tus comentarios! + Si se activa, siempre aparecerás como fuera de línea para otros usuarios, incluso cuando uses la aplicación. + Los hilos ayudan a mantener tus conversaciones en el asunto y las hacen más fáciles de rastrear. %sHabilitar la función de hilos refrescará la aplicación. Esto podría tardar más en algunas cuentas. + Notificación de sala + Usuarios + Notificar a toda la sala + La compartición de ubicación está en progreso + ${app_name} Ubicación en directo + Cuando se active serás capaz de enviar tu ubicación a cualquier sala + Activar compartir ubicación + Abrir con + ${app_name} no ha podido acceder a tu ubicación. Por favor, inténtalo de nuevo más tarde. + ${app_name} no ha podido acceder a tu ubicación + If te gustaría compartir tu ubicación en tiempo real, ${app_name} necesita permiso de ubicación siempre cuando la aplicación esté en segundo plano +\nSolo acederemos a tu ubicación para la duración que escogas. + Compartir tu ubicación en directo para + Compartir esta ubicación + Compartir ubicación en directo + Compartir mi ubicación actual + Hacer zoom a ubicación actual + Pin de ubicación selecionada en el mapa + Mapa + Compartir ubicación + Ubicación + Compartir ubicación + Los resultados solo se revelan cuando finalices la encuesta + Encuesta cerrada + Los votantes verán los resultados tan pronto como hayan votado + Abrir encuesta + Tipo de encuesta + Editar encuesta + ¿Estás seguro de que quieres eliminar esta encuesta\? No podrás recuperarla cuando se elimine. + Eliminar encuesta + Encuesta finalizada + Voto enviado + Finalizar encuesta + Esto evitará que las personas puedan votar y mostrará los resultados finales de la encuesta. + ¿Finalizar encuesta\? + opción ganadora + Finalizar encuesta + + Resultado final basado en %1$d voto + Resultado final basado en %1$d votos + + + %1$d voto enviado. Vota para ver los resultados + %1$d votos enviados. Vota para ver los resultados + + No hay ningún voto + + Basado en %1$d voto + Basado en %1$d votos + + + %1$d voto + %1$d votos + + + Al menos %1$s opción es requerida + Al menos %1$s opciones son requeridas + + La pregunta no puede estar vacía + CREAR ENCUESTA + AÑADIR OPCIÓN + Opción %1$d + Crear opciones + Pregunta o asunto + Pregunta o asunto de encuesta + Crear Encuesta + Reinicia la aplicación para que el cambio surta efecto. + Ayúdanos a identificar problemas y a mejorar ${app_name} compartiendo datos de uso anónimos. Para comprender como las personas usan múltiples dispositivos, generaremos un identificador aleatorio, compartido por tus dispositivos. +\n +\nPuedes leer todos nuestros términos en %s. + Habilitar matemáticas con LaTeX + (%1$s) + %1$s (%2$s) + Ocurrió un error al reproducir %1$s + Pausar %1$s + Reproducir %1$s + %1$d minutos %2$d segundos + %1$s,%2$s,%3$s + Nota: la applicación se reiniciará + Activar mensajes en hilo + Tu sistema automáticamente enviará registros cuando ocurra un fallo al desencriptar + Reportar errores de desencriptado automáticamente. + No disponible + Fuera de línea + En línea + El servidor doméstico no acepta nombres de usuario con únicamente dígitos. + La encriptación está configurada de manera incorrecta + Sobreescribir color del nick + Restaurar Encriptación + Por favor, contacta con un administrador para restablecer la encriptación a un estado válido. + La encriptación ha sido configurada de forma incorrecta. + Compartieron su localización en vivo + Compartieron su localización + Puedes cambiar esto en todo momento. + Esto se mostrará cuando mandes mensajes. + Llévame a casa + Personalizar perfil + Conectar a servidor + ¿Querías unirte a un servidor ya existente\? + Encriptado extremo-a-extremo, sin requerir un número de teléfono. Sin anuncios o minado de datos. + Elige dónde se almacenan tus conversaciones, dándote control e independencia. Conectado mediante Matrix. + La comunicación segura e independiente que te da el mismo nivel de privacidad que una conversación cara a cara en tu propio hogar. + Enviar emials y números de teléfono a %s + Tus contactos son privados. Para descubrir usuarios desde tus contactos, necesitamos tu permiso para enviar información de contactos a tu servidor de identidad. + BETA + Comentarios de la beta de hilos + Beta de hilos \ No newline at end of file diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 19b4d37102..79eee81fa9 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -743,7 +743,9 @@ \nÜldistest seadistustest saad seda alati muuta. Eira selle kasutaja sõnumeid Lõpeta selle kasutaja eiramine - Selle kasutaja eiramise lõpetamine teeb tema sõnumid uuesti nähtavaks. + Selle kasutaja eiramise lõpetamine teeb tema sõnumid uuesti nähtavaks. +\n +\nPalun arvesta, et samaga käivitud rakendub uuesti ning andmete sünkroniseerimiseks kulub natuke aega. Lõpeta eiramine Tühista kutse Kas oled kindel et sa soovid tühistada kutse sellele kasutajale\? @@ -2457,4 +2459,32 @@ Mine avalehele Isikupärasta oma profiili Lülita välja + Reaalajas asukoha laadimine… + 8 tunni kestel + 1 tunni kestel + 15 minuti jooksul + Jaga oma asukohta reaalajas + (%1$s) + %1$s (%2$s) + %1$s helisõnumi esitamine ei õnnestu + Peata helisõnumi esitus: %1$s + Esita helisõnumit: %1$s + %1$d minutit %2$d sekundit + %1$s, %2$s, %3$s + Jagas oma asukohta + ${app_name} sobib ideaalselt kasutamiseks töökeskkonnas. Ta on kasutusel ka mitmetes üliturvalistes organisatsioonides. + BEETA + Jutulõngade beetaversiooni tagasiside + Jaga tagasisidet + BEETA + Kui see valik on kasutusel, siis sa alati oled teiste jaoks võrgust väljas. Seda ka siis, kui kasutad rakendust. + Ei ole võrgus + Olek võrgus + Sinu koduserver hetkel ei toeta jutulõngasid ning seega antud funktsionaalsus ei pruugi toimida korralikult. Kõik sõnumid jutulõngas ilmselt ei ole loetavad. %sKas sa ikkagi soovid jutulõngad kasutusele võtta\? + Jutulõngade beetaversioon + Jutulõngade beetaversioon + Lisateave + Proovi nüüd + Jutulõngad on hetkel arendusjärgus funktsionaalsus ning samaga lisandub ka senisest parem versioon teavitustest. Me hea meelega tahaksime kuulda, mida sa nendest muutustest arvad! + Jutulõngad aitavad hoida sinu vestlusi teemakohastena ning kergesti jälgitavatena.%sJutulõngade kasutusele võtmisel laadime rakenduse uuesti. Kui sul on väga mahukad kontod, siis võib natuke aega kuluda. \ No newline at end of file diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index 9a23913f76..4d01fe269b 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -1256,7 +1256,9 @@ اگر مدیر کارساز خبر از مورد انتظار بودنش داده، مطمئن شوید که اثر انگشت زیر با اثر انگشت ارائه شده به دستشان مطابق است. می‌تواند به این معنی باشد که کسی شدامدتان را بدخواهانه دستکاری کرده یا تلفنتان، به گواهی فراهم‌شده به دست کارساز دوردست، اطمینان ندارد. با لغو مسدودیت، کاربر می‌تواند مجددا به اتاق اضافه شود. - لغو نادیده‌گیری کاربر سبب می‌شود دوباره همه پیام‌های او نمایش داده شود. + ناچشم‌پوشی این کاربر موجب نمایانی تمامی پیام‌ها از سویش خواهد شد. +\n +\nبه خاطر داشته باشید که این کنش کاره را دوباره آغاز خواهد کرد و ممکن است مدّتی زمان ببرد. نادیده‌گرفتن این کاربر پیام‌هایش را از اتاق‌های مشترکتان حذف خواهد کرد. \n \nهرگاه که بخواهید می‌توانید این کنش را در تنظیمات کلی لغو کنید. @@ -2457,4 +2459,32 @@ مرا به خانه ببر شخصی سازی نمایه از کار انداختن + رشته‌ها کاری در حال پیشرفت با ویژگی‌های جذّاب جدید مانند آگاهی‌های بهبودیافته هستند. دوست داریم بازخوردتان را بشنویم! + کارساز خانگیتان در حال حاضر از رشته‌ها پشتیبانی نمی‌کند؛ بنابراین ممکن است این ویژگی قابل اتّکا نباشد. ممکن است برخی پیام‌های رشته‌ای همواره موجود نباشند. %sدر هر صورت می‌خواهید رشته‌ها را به کار بیندازید؟ + رشته‌ها به روی موضوع نگه داشتن گفت‌وگوها کمک کرده و ردیابیشان را آسان می‌کنند. %sبه کارانداختن رشته‌ها کاره را دوباره آغاز خواهد کرد. ممکن است این کار برای برخی حساب‌ها بیش‌تر طول بکشد. + بار کردن مکان زنده… + ۸ ساعت + ۱ ساعت + ۱۵ دقیقه + هم‌رسانی مکان زنده‌تان برای + (%1$s) + %1$s (%2$s) + ناتوان در پخش %1$s + مکث %1$s + پخش %1$s + %1$d دقیقه و %2$d ثانیه + %1$s، %2$s، %3$s + مکان زنده‌اش را هم‌رساند + ${app_name} برای محل کار نیز عالیست. مورد اعتماد امن‌ترین سازمان‌های جهان است. + آزمایشی + بازخورد رشته‌های آزمایشی + بازخورد دادن + آزمایشی + اگر به کار افتاده باشد، برای دیگر کاربران، همواره برون‌خط ظاهر خواهید شد، حتا هنگام استفاده از برنامه. + حالت برون‌خط + حضور + رشته‌های آزمایشی + رشته‌های آزمایشی + بیش تر بدانید + بیازماییدش \ No newline at end of file diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 33bde2944e..3a8184cef7 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1486,7 +1486,9 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze \n \nEzt a műveletet bármikor visszavonhatja az általános beállításokban. Felhasználó figyelembe vétele - A felhasználó figyelembe vétele után újra meg fog jelenni az összes üzenete. + A felhasználó figyelembe vétele után újra meg fog jelenni az összes üzenete. +\n +\nEz a művelet újraindítja az alkalmazást ami sokáig tarthat. Meghívás visszavonása Biztos, hogy visszavonja a felhasználó meghívását\? a felhasználó kirúgása eltávolítja a szobából. @@ -2457,4 +2459,32 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Vigyél haza Profil személyre szabása Tiltás + Élő földrajzi helyzet meghatározás betöltése… + 8 óra + 1 óra + 15 perc + Folyamatos helyzet megosztása ezért: + (%1$s) + %1$s (%2$s) + Nem lehet lejátszani ezt: %1$s + Megállít: %1$s + Lejátszás: %1$s + %1$d perc %2$d másodperc + %1$s, %2$s, %3$s + A földrajzi helyzetüket folyamatosan megosztják + ${app_name} megállja a helyét a munkahelyen is. A világ legbiztonságosabb szervezetei bíznak meg benne. + Béta + Üzenetszálak fejlesztése folyamatban van, új és izgalmas funkciókkal, mint a fejlesztett értesítések. Szeretnénk hallani a véleményed! + Béta Üzenetszálak visszajelzés + Visszajelzés adása + Béta + Ha engedélyezed úgy látszol majd mások számára, mintha nem kapcsolódnál a hálózathoz még akkor is amikor az alkalmazást használod. + Hálózati kapcsolat nélkül + Állapot + A Matrix szervered jelenleg nem támogatja az üzenetszálakat így ez a funkció nem lesz megbízható. Bizonyos üzenetszálas üzenetek nem jelennek meg megbízhatóan. %sBiztos, hogy így is engedélyezed az üzenetszálakat\? + Béta Üzenetszálak + Az üzenetszálak segítenek a különböző témájú beszélgetések figyelemmel kísérésében. %sÜzenetszálak engedélyezése újraindítja az alkalmazást. Ez néhány fióknál sokáig tarthat. + Béta Üzenetszálak + Tudj meg többet + Próbáld ki \ No newline at end of file diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index 5ca1a361d4..f0533eba10 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -72,7 +72,7 @@ Gabung Tolak Nanti - Kirim catat gangguan + Kirim catatan gangguan Raksasa Kecil Normal @@ -87,7 +87,7 @@ Gandakan ke clipboard Kirim tampilan layar Mohon uraikan kutu tersebut. Apa yang Anda lakukan\? Apa yang Anda harapkan terjadi\? Apa yang sebenarnya terjadi\? - Catat dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk catat dan tangkapan layar, tidak akan terlihat secara umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silakan hapus centang: + Catatan dari klien akan dikirim bersama laporan gangguan ini untuk mendalami kendala yang Anda temukan. Laporan gangguan ini, termasuk catatan dan tangkapan layar, tidak akan terlihat secara umum. Jika Anda hanya ingin mengirimkan tulisan di atas, silakan hapus centang: Sepertinya Anda mengguncang ponsel akibat frustrasi. Apakah Anda ingin membuka halaman laporan kutu\? Pengiriman laporan kutu gagal (%s) Kemajuan (%s%%) @@ -590,7 +590,9 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Keluarkan pengguna Apakah Anda yakin ingin membatalkan undangan untuk pengguna ini\? Batalkan undangan - Membatalkan abaian pengguna ini akan menampilkan semua pesan dari mereka. + Membatalkan abaian pengguna ini akan menampilkan semua pesan dari mereka. +\n +\nDicatat bahwa tindakan ini akan memulai ulang aplikasi dan mungkin membutuhkan beberapa waktu. Batal pengabaian pengguna Mengabaikan pengguna ini akan menghilangkan pesan mereka dari ruangan yang Anda bagikan. \n @@ -1179,8 +1181,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kirim lampiran Sepertinya server terlalu lama merespons, hal ini dapat disebabkan oleh konektivitas yang buruk atau kesalahan pada server. Silakan coba lagi dalam beberapa saat. Silakan coba lagi setelah Anda menerima syarat dan ketentuan homeserver Anda. - Log verbose akan membantu pengembang dengan menyediakan lebih banyak catat saat Anda mengirim RageShake. Bahkan ketika diaktifkan, aplikasi tidak mencatat isi pesan atau data pribadi lainnya. - Aktifkan catat verbose. + Catatan verbose akan membantu pengembang dengan menyediakan lebih banyak catat saat Anda mengirim RageShake. Bahkan ketika diaktifkan, aplikasi tidak mencatat isi pesan atau data pribadi lainnya. + Aktifkan catatan verbose. Kode verifikasi salah. Kode Sebuah pesan teks telah dikirim ke %s. Silakan masukkan kode verifikasi yang ada di dalamnya. @@ -2285,7 +2287,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Mulai ulang aplikasi ini untuk menerapkan perubahan. Aktifkan matematika LaTeX Anda tidak diperbolehkan untuk bergabung ke ruangan ini - Sistem Anda akan mengirimkan catat secara otomatis ketika sebuah kesalahan dekripsi terjadi + Sistem Anda akan mengirimkan catatan secara otomatis ketika sebuah kesalahan dekripsi terjadi Buat poll Buka kontak Kirim stiker @@ -2345,7 +2347,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Siapa saja yang sering Anda chat\? Anda sudah menampilkan utasan ini! Tampilkan Di Ruangan - Balas Di Utasan + Balas di utasan Perintah \"%s\" dikenal tetapi tidak didukung dalam utasan. Dari sebuah Utasan Tip: Tekan lama pada sebuah pesan dan gunakan “%s”. @@ -2412,4 +2414,32 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kembalikan saya ke beranda Ubah profil Nonaktifkan + Memuat lokasi langsung… + 8 jam + 1 jam + 15 menit + Bagikan lokasi langsung Anda untuk + (%1$s) + %1$s (%2$s) + Tidak dapat memainkan %1$s + Jeda %1$s + Mainkan %1$s + %1$d menit %2$d detik + %1$s, %2$s, %3$s + Membagikan lokasi langsungnya + ${app_name} juga baik untuk tempat kerja. Dipercayai oleh organisasi yang paling aman di dunia. + BETA + Utasan adalah fitur yang sedang dikerjakan dengan fitur baru dan asik. Kami ingin dengar masukan Anda! + Masukan Utasan Beta + Berikan masukan + BETA + Jika diaktifkan, Anda akan selalu terlihat luring kepada pengguna lain, bahkan ketika menggunakan aplikasi. + Mode luring + Presensi + Homeserver Anda saat ini tidak mendukung utasan, jadi fitur ini mungkin tidak andal. Beberapa pesan yang diutas mungkin tidak tersedia. %sApakah Anda ingin mengaktifkan utasan saja\? + Utasan Beta + Utasan membantu percakapan sesuai topik dan mudah untuk dilacak. %sMengaktifkan utasan akan memuat aplikasi ulang. Ini mungkin membutuhkan beberapa waktu untuk beberapa akun. + Utasan Beta + Pelajari lebih lanjut + Coba \ No newline at end of file diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml index 90446c8819..f052e88ee5 100644 --- a/vector/src/main/res/values-is/strings.xml +++ b/vector/src/main/res/values-is/strings.xml @@ -1350,7 +1350,7 @@ Skilaboð frá vélmennum Boð á spjallrás Stikkorð - \@spjallrás + \@room Hópskilaboð Bein skilaboð Notandanafnið mitt @@ -1446,7 +1446,7 @@ Láta mig vita fyrir Uppgötvun Uppfærslur spjallrásar - Skilaboð sem innihalda @spjallrás + Skilaboð sem innihalda @room Þegar spjallrásir eru uppfærðar Heimildir svæðis Ástæða fyrir banni diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 15f08ba6da..66fff2077e 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1519,7 +1519,9 @@ \n \nPuoi annullare questa azione in qualsiasi momento nelle impostazioni generali. Non ignorare più - Se non ignori più l\'utente vedrai di nuovo tutti i suoi messaggi. + Se non ignori più l\'utente vedrai di nuovo tutti i suoi messaggi. +\n +\nNota che quest\'azione riavvierà l\'app e potrebbe richiedere del tempo. Annulla invito Sei sicuro di voler annullare l\'invito per questo utente\? Butta fuori l\'utente @@ -2448,4 +2450,32 @@ Congratulazioni! Personalizza profilo Disattiva + Caricamento posizione in tempo reale… + 8 ore + 1 ora + 15 minuti + Condividi la tua posizione in tempo reale per + (%1$s) + %1$s (%2$s) + Impossibile riprodurre %1$s + Metti in pausa %1$s + Riproduci %1$s + %1$d minuti %2$d secondi + %1$s, %2$s, %3$s + Ha condiviso la sua posizione + ${app_name} è ottimo anche per i luoghi di lavoro. È considerato affidabile dalle più sicure organizzazioni del mondo. + BETA + Le conversazioni sono ancora in sviluppo e ci saranno nuove funzionalità in arrivo, come le notifiche migliorate. Vorremmo sapere la tua opinione! + Feedback della beta conversazioni + Invia un feedback + BETA + Se attiva, apparirai sempre offline agli altri utenti, anche quando usi l\'applicazione. + Modalità offline + Presenza + Il tuo homeserver attualmente non supporta i messaggi in conversazioni, perciò questa funzione sarà inaffidabile. Alcuni messaggi in conversazioni potrebbero non essere disponibili. %sVuoi attivare comunque le conversazioni\? + Beta conversazioni + Le conversazioni ti aiutano a tenere le tue discussioni in tema e rintracciabili. %sL\'attivazione ricaricherà l\'app. Potrebbe richiedere più tempo per alcuni account. + Beta conversazioni + Maggiori informazioni + Provalo \ No newline at end of file diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 672e2472bb..49335373a0 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -512,7 +512,7 @@ セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ - (編集済) + (編集した) 会話を検索… 全てのメッセージ (音量大) 全てのメッセージ @@ -962,7 +962,7 @@ インテグレーション(統合) アプリがバックグラウンドにある場合、着信メッセージは通知されません。 ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 -\nこれは無線とバッテリーの使用量に影響し、$ {app_name}がイベントを待機していることを示す永続的な通知が表示されます。 +\nこれは無線とバッテリーの使用量に影響し、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期をします。 \n端末の状態によっては、OSによって同期が延期される場合があります。 LEDの色、振動、音を選択してください… @@ -2372,4 +2372,4 @@ \nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 スレッドはベータ版になります 🎉 無効にする - \ No newline at end of file + diff --git a/vector/src/main/res/values-lo/strings.xml b/vector/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..72b2ae1d21 --- /dev/null +++ b/vector/src/main/res/values-lo/strings.xml @@ -0,0 +1,2323 @@ + + + ການລົງທະບຽນ Token + ເພີ່ມບັນຊີ + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ຂອງ ${app_name}. ບໍ່ມີບັນຊີ Google ຢູ່ໃນໂທລະສັບ. ກະລຸນາເປີດຕົວຈັດການບັນຊີ ແລະເພີ່ມບັນຊີ Google. + ດຶງຂໍ້ມູນໂທເຄັນ FCMບໍ່ສຳເລັດ: +\n%1$s + ດຶງຂໍ້ມູນໂທເຄັນ FCM ສຳເລັດແລ້ວ: +\n%1$s + Firebase Token + ແກ້ໄຂບໍລິການຫຼິ້ນ + ${app_name} ໃຊ້ Google Play Services ເພື່ອສົ່ງຂໍ້ຄວາມ ແຕ່ເບິ່ງຄືວ່າບໍ່ໄດ້ຖືກຕັ້ງຄ່າຢ່າງຖືກຕ້ອງ: +\n%1$s + Google Play Services APK ສາມາດໃຊ້ໄດ້ ແລະ ອັບເດດແລ້ວ. + ກວດສອບບໍລິການການຫຼິ້ນ + ການແຈ້ງເຕືອນບາງລາຍການຖືກປິດໃຊ້ງານໃນການຕັ້ງຄ່າແບບກຳນົດເອງຂອງທ່ານ. + ສັງເກດເຫັນວ່າຂໍ້ຄວາມບາງປະເພດໄດ້ຖືກຕັ້ງໃຫ້ເປັນແບບບໍ່ມີສຽງ (ຈະສ້າງການແຈ້ງການທີ່ບໍ່ມີສຽງ). + ການຕັ້ງຄ່າແບບກຳນົດເອງ. + ເປີດໃຊ້ + ບໍ່ໄດ້ເປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບລະບົບນີ້. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າ ${app_name}. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ງານສໍາລັບລະບົບນີ້. + ການຕັ້ງຄ່າລະບົບ. + ເປີດໃຊ້ + ການແຈ້ງເຕືອນຖືກປິດການນຳໃຊ້ສຳລັບບັນຊີຂອງທ່ານ. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າບັນຊີ. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ງານສຳລັບບັນຊີຂອງທ່ານ. + ຕັ້ງຄ່າບັນຊີ. + ເປີດການຕັ້ງຄ່າ + ການແຈ້ງເຕືອນຖືກປິດໃຊ້ງານຢູ່ໃນການຕັ້ງຄ່າລະບົບ. +\nກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າລະບົບ. + ການແຈ້ງເຕືອນຖືກເປີດໃຊ້ໃນການຕັ້ງຄ່າລະບົບ. + ການຕັ້ງຄ່າລະບົບ. + ການທົດສອບໜຶ່ງ ຫຼື ຫຼາຍກວ່ານັ້ນບໍ່ສຳເລັດ, ກະລຸນາສົ່ງລາຍງານຂໍ້ຜິດພາດເພື່ອຊ່ວຍພວກເຮົາກວດສອບ. + ການທົດສອບຫນຶ່ງ ຫຼື ຫຼາຍກວ່ານັ້ນບໍ່ສຳເລັດ, ລອງວິທີແກ້ໄຂທີ່ໄດ້ແນະນໍາ. + ການວິນິດໄສພື້ນຖານແມ່ນ OK. ຖ້າທ່ານຍັງບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນ, ກະລຸນາສົ່ງລາຍງານຂໍ້ຜິດພາດເພື່ອຊ່ວຍພວກເຮົາສືບສວນ. + ກຳລັງດຳເນິນການ… (%1$d ຂອງ %2$d) + ດໍາເນີນການທົດສອບ + ການແກ້ໄຂບັນຫາດ້ວຍການວິນິດໄສ + ແກ້ໄຂບັນຫາການແຈ້ງເຕືອນ + ຄໍາສໍາຄັນບໍ່ສາມາດມີ \'%s\' + ຄໍາສໍາຄັນບໍ່ສາມາດເລີ່ມຕົ້ນດ້ວຍ \'.\' + ເພີ່ມຄໍາສໍາຄັນໃຫມ່ + ຄໍາສໍາຄັນຂອງທ່ານ + ແຈ້ງໃຫ້ຂ້າພະເຈົ້າຊາບສໍາລັບ + ອື່ນໆ + ການກ່າວເຖິງແລະຄໍາສໍາຄັນ + ການແຈ້ງເຕືອນເລີ່ມຕົ້ນ + ເປີດໃຊ້ການແຈ້ງເຕືອນອີເມວສຳລັບ %s + ເພື່ອຮັບອີເມວພ້ອມການແຈ້ງເຕືອນ, ກະລຸນາເຊື່ອມຕໍ່ອີເມວເຂົ້າບັນຊີ Matrix ຂອງທ່ານ + ການແຈ້ງເຕືອນທາງອີເມວ + ຄວາມສຳຄັນຂອງການແຈ້ງເຕືອນຕາມເຫດການ + ການຕັ້ງຄ່າການແຈ້ງເຕືອນຂັ້ນສູງ + ໃຫ້ແນ່ໃຈວ່າທ່ານໄດ້ກົດໃສ່ການເຊື່ອມຕໍ່ໃນອີເມວທີ່ພວກເຮົາໄດ້ສົ່ງໃຫ້ທ່ານ. + ລຶບ ອອກບໍ%s\? + ເບີໂທລະສັບ + ບໍ່ມີອີເມວໃດຖືກເພີ່ມໃສ່ບັນຊີຂອງທ່ານ + ທີ່ຢູ່ອີເມວ + ສະແດງຂໍ້ມູນລະບົບການຕັ້ງຄ່າແອັບພລິເຄຊັນ. + ຂໍ້ມູນການສະຫມັກ + ເພີ່ມເບີໂທລະສັບ + ບໍ່ມີເບີໂທລະສັບທີ່ຖືກເພີ່ມໃສ່ບັນຊີຂອງທ່ານ + ເພີ່ມທີ່ຢູ່ອີເມວ + ສະແດງຊື່ + ຮູບໂປຣໄຟລ໌ + ໄດ້ອອກຈາກລະບົບແລ້ວ! + ອອກຈາກຫ້ອງແລ້ວ! + ເພີ່ມໃສ່ໜ້າຈໍຫຼັກ + ບໍ່ມີ + ການກ່າວເຖິງ & ຄຳສັບສຳຄັນເທົ່ານັ້ນ + ຂໍ້ຄວາມທັງໝົດ + ບໍ່ມີຜົນ + ການກັ່ນຕອງຫ້າມຜູ້ໃຊ້ + ການກັ່ນຕອງສະມາຊິກຫ້ອງ + ຊອກຫາ + homeserver ຂອງທ່ານບໍ່ຮອງຮັບກະທູ້ໃນປັດຈຸບັນ, ດັ່ງນັ້ນຄຸນສົມບັດນີ້ອາດຈະບໍ່ໜ້າເຊື່ອຖື. ບາງຂໍ້ຄວາມທີ່ເປັນກະທູ້ອາດຈະບໍ່ມີຄວາມໜ້າເຊື່ອຖືໄດ້. %sທ່ານຕ້ອງການເປີດໃຊ້ຫົວຂໍ້ແນວໃດ\? + ກະທູ້ເບຕ້າ + ກະທູ້ຊ່ວຍໃຫ້ການສົນທະນາຂອງທ່ານຢູ່ໃນຫົວຂໍ້ແລະງ່າຍຕໍ່ການຕິດຕາມ. %s ການເປີດໃຊ້ງານກະທູ້ຈະໂຫຼດແອັບຄືນໃໝ່. ອັນນີ້ອາດຈະໃຊ້ເວລາດົນກວ່າສໍາລັບບາງບັນຊີ. + ກະທູ້ເບຕ້າ + ພວກເຮົາໃກ້ຈະອອກ ຈາກກະທູ້ Beta ສາທາລະນະ. +\n +\nໃນຂະນະທີ່ພວກເຮົາກະກຽມສໍາລັບສິ່ງນີ້, ພວກເຮົາຈໍາເປັນຕ້ອງເຮັດການປ່ຽນແປງບາງຢ່າງ: ກະທູ້ທີ່ສ້າງຂຶ້ນກ່ອນນີ້ຈະຖືກສະແດງເປັນການຕອບປົກກະຕິ. +\n +\nສິ່ງນີ້ຈະເປັນການປ່ຽນແປງຄັ້ງດຽວເນື່ອງຈາກກະທູ້ເປັນສ່ວນໜຶ່ງຂອງຂໍ້ມູນສະເພາະຂອງ Matrix. + ກະທູ້ໃກ້ຮອດເບຕ້າ 🎉 + ຈາກກະທູ້ + ຄຳແນະນຳ: ແຕະໃສ່ຂໍ້ຄວາມດົນໆ ແລະໃຊ້ “%s”. + ກະທູ້ຊ່ວຍໃຫ້ການສົນທະນາຂອງທ່ານຢູ່ໃນຫົວຂໍ້ ແລະ ງ່າຍຕໍ່ການຕິດຕາມ. + ຈັດລະບຽບການສົນທະນາດ້ວຍກະທູ້ + ສະແດງຫົວຂໍ້ທັງໝົດທີ່ທ່ານໄດ້ເຂົ້າຮ່ວມ + ກະທູ້ຂອງຂ້ອຍ + ສະແດງຫົວຂໍ້ທັງໝົດຈາກຫ້ອງປັດຈຸບັນ + ກະທູ້ທັງໝົດ + ການກັ່ນຕອງ + ກະທູ້ + ກະທູ້ + ການກັ່ນຕອງກະທູ້ຢູ່ໃນຫ້ອງ + + ເລືອກ %d ແລ້ວ + + ປ່ຽນຫົວຂໍ້ + ຍົກລະດັບພື້ນທີ່ + ຍົກລະດັບຫ້ອງ + ສົ່ງເຫດການ m.room.server_acl + ປ່ຽນການອະນຸຍາດ + ປ່ຽນຊື່ພຶ້ນທີ່ + ປ່ຽນຊື່ຫ້ອງ + ປ່ຽນການເບິ່ງເຫັນປະຫວັດ + ເປີດການເຂົ້າລະຫັດພື້ນທີ່ + ເປີດການເຂົ້າລະຫັດຫ້ອງ + ປ່ຽນທີ່ຢູ່ຫຼັກສຳລັບພື້ນທີ່ + ປ່ຽນທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງ + ປ່ຽນຮູບແທນຕົວຂອງພຶ້ນທີ່ + ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + ແກ້ໄຂ widget + ແຈ້ງເຕືອນທຸກຄົນ + ລຶບຂໍ້ຄວາມທີ່ຄົນອື່ນສົ່ງມາ + ຫ້າມຜູ້ໃຊ້ + ລຶບຜູ້ໃຊ້ອອກ + ປ່ຽນການຕັ້ງຄ່າ + ເຊີນຜູ້ໃຊ້ + ສົ່ງຂໍ້ຄວາມ + ບົດບາດເລີ່ມຕົ້ນ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ປັບປຸງບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່ນີ້ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ປັບປຸງບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ + ເລືອກບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່ນີ້ + ເລືອກບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ + ການອະນຸຍາດ + ເບິ່ງ ແລະອັບເດດບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງພື້ນທີ່. + ເບິ່ງ ແລະ ອັບເດດບົດບາດທີ່ຕ້ອງການປ່ຽນພາກສ່ວນຕ່າງໆຂອງຫ້ອງ. + ການອະນຸຍາດພື້ນທີ່ + ການອະນຸຍາດຫ້ອງ + ຍອມຮັບໃບຢັ້ງຢືນຖ້າຜູ້ເບິ່ງແຍງເຊີບເວີໄດ້ເຜີຍແຜ່ລາຍນິ້ວມືທີ່ກົງກັບຂ້າງເທິງ. + ຖ້າຫາກຜູ້ເບິ່ງແຍງເຊີບເວີໄດ້ບອກວ່າເປັນເຊັ່ນນີ້, ໃຫ້ແນ່ໃຈວ່າລາຍນິ້ວມືຂ້າງລຸ່ມນີ້ກົງກັບລາຍນິ້ວມືທີ່ເຂົາເຈົ້າສະໜອງໃຫ້. + ອັນນີ້ອາດໝາຍຄວາມວ່າມີຄົນຂັດຂວາງການຮັບສົ່ງຂອງທ່ານຢ່າງເຈດຕະນາ, ຫຼືວ່າໂທລະສັບຂອງທ່ານບໍ່ມີຄວາມໜ້າເຊື່ອຖືການຢັ້ງຢືນທີ່ສະໜອງໃຫ້ໂດຍເຊີບເວີທາງໄກ. + ບໍ່ສາມາດຢືນຢັນຕົວຕົນຂອງເຊີບເວີທາງໄກໄດ້. + ລາຍນິ້ວມື (%s): + ບໍ່ສົນໃຈ + ອອກຈາກລະບົບ + ເຊື່ອຖືບໍ່ໄດ້ + ເຊື່ອຖືໄດ້ + + %d ຂໍ້ຄວາມໃໝ່ + + ການຕັ້ງຄ່າເຂົ້າລະຫັດບໍ່ຖືກຕ້ອງ ດັ່ງນັ້ນທ່ານບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້. ກົດເພື່ອເປີດການຕັ້ງຄ່າ. + ຕັ້ງຄ່າການເຂົ້າລະຫັດບໍ່ຖືກຕ້ອງ ດັ່ງນັ້ນທ່ານບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້. ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບເພື່ອກູ້ຄືນການເຂົ້າລະຫັດໃຫ້ເປັນສະຖານະທີ່ຖືກຕ້ອງ. + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ໂພສໃສ່ຫ້ອງນີ້. + %1$s, %2$s ແລະ ອື່ນໆ + %1$s ແລະ %2$s + %1$s & %2$s ແລະ ຄົນອື່ນໆກຳລັງພິມ… + %1$s & %2$s ກຳລັງພິມ… + %s ກຳລັງພິມ… + ການຍົກເລີກການຫ້າມຜູ້ໃຊ້ຈະອະນຸຍາດໃຫ້ພວກເຂົາເຂົ້າຮ່ວມພື້ນທີ່ອີກເທື່ອຫນຶ່ງ. + ການຍົກເລີກຜູ້ໃຊ້ຈະອະນຸຍາດໃຫ້ພວກເຂົາເຂົ້າຮ່ວມຫ້ອງອີກເທື່ອຫນຶ່ງ. + ການຫ້າມຜູ້ໃຊ້ຈະລຶບພວກເຂົາອອກຈາກພື້ນທີ່ນີ້ ແລະປ້ອງກັນບໍ່ໃຫ້ເຂົາເຈົ້າເຂົ້າຮ່ວມອີກຄັ້ງ. + ການຫ້າມຜູ້ໃຊ້ຈະລຶບເຂົາເຈົ້າອອກຈາກຫ້ອງນີ້ ແລະປ້ອງກັນບໍ່ໃຫ້ເຂົາເຈົ້າເຂົ້າຮ່ວມອີກຄັ້ງ. + ຍົກເລີກການຫ້າມຜູ້ໃຊ້ + ເຫດຜົນທີ່ຫ້າມ + ຫ້າມຜູ້ໃຊ້ + ຜູ້ໃຊ້ຈະຖືກລຶບອອກຈາກພື້ນທີ່ນີ້. +\n +\nເພື່ອປ້ອງກັນບໍ່ໃຫ້ພວກເຂົາເຂົ້າຮ່ວມອີກເທື່ອຫນຶ່ງ, ທ່ານຄວນຫ້າມພວກເຂົາແທນ. + ຜູ້ໃຊ້ຈະຖືກລຶບອອກຈາກຫ້ອງນີ້. +\n +\nເພື່ອປ້ອງກັນບໍ່ໃຫ້ພວກເຂົາເຂົ້າຮ່ວມອີກເທື່ອຫນຶ່ງ, ທ່ານຄວນຫ້າມພວກເຂົາແທນ. + ເຫດຜົນທີ່ຈະລຶບອອກ + ເອົາຜູ້ໃຊ້ອອກ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການຍົກເລີກການເຊີນຜູ້ໃຊ້ນີ້\? + ຍົກເລີກການເຊີນ + ຍົກເລີກການບໍ່ສົນໃຈ + ການບໍ່ສົນໃຈຜູ້ໃຊ້ນີ້ຈະສະແດງຂໍ້ຄວາມທັງໝົດຈາກເຂົາເຈົ້າອີກຄັ້ງ. +\n +\nກະລຸນາຮັບຊາບວ່າຄຳສັ່ງນີ້ຈະປິດເປີດແອັບຄືນໃໝ່ ແລະ ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ບໍ່ສົນໃຈ + ການບໍ່ສົນໃຈຜູ້ໃຊ້ນີ້ຈະລຶບຂໍ້ຄວາມຂອງເຂົາເຈົ້າອອກຈາກຫ້ອງທີ່ທ່ານແບ່ງປັນ. +\n +\nທ່ານສາມາດຍົກເລີກການກະທຳນີ້ໄດ້ທຸກເວລາໃນການຕັ້ງຄ່າທົ່ວໄປ. + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ຫຼຸດລະດັບ + ທ່ານຈະບໍ່ສາມາດຍົກເລີກການປ່ຽນແປງນີ້ໄດ້ໃນຂະນະທີ່ທ່ານກໍາລັງ ລຸດລະດັັບຕົວທ່ານເອງ, ຖ້າທ່ານເປັນຜູ້ໃຊ້ສິດທິພິເສດສຸດທ້າຍຢູ່ໃນຫ້ອງ, ມັນຈະເປັນໄປບໍ່ໄດ້ທີ່ຈະຄືນສິດທິພິເສດ. + ຫຼຸດລະດັບຕົວເອງບໍ\? + ທ່ານຈະບໍ່ສາມາດຍົກເລີກການປ່ຽນແປງນີ້ໄດ້ເນື່ອງຈາກທ່ານກໍາລັງສົ່ງເສີມຜູ້ໃຊ້ໃຫ້ມີລະດັບພະລັງງານດຽວກັນກັບຕົວທ່ານເອງ. +\nທ່ານແນ່ໃຈບໍ່\? + ກ່າວເຖິງ + ລຶບອອກຈາກການສົນທະນາ + ຍົກເລີກການຫ້າມ + ຫ້າມ + ຍົກເລີກການເຊີນ + ເຊີນ + ຂໍ້ຄວາມໂດຍກົງ + ຫ້ອງນີ້ບໍ່ແມ່ນສາທາລະນະ. ທ່ານຈະບໍ່ສາມາດເຂົ້າຮ່ວມຄືນໃໝ່ໄດ້ໂດຍບໍ່ມີການເຊີນ. + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກຫ້ອງ\? + ອອກຈາກຫ້ອງ + + ສະມາຊິກ %d + + ໄປຫາລາຍການທີ່ຍັງບໍ່ໄດ້ອ່ານ + ສະມາຊິກ + ສືບຕໍ່ + ບໍ່ + ແມ່ນແລ້ວ + ອະນຸຍາດໃຫ້ເຂົ້າເຖິງລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານ. + ເພື່ອສະແກນລະຫັດ QR, ທ່ານຈໍາເປັນຕ້ອງອະນຸຍາດໃຫ້ເຂົ້າເຖິງກ້ອງຖ່າຍຮູບ. + ${app_name} ຕ້ອງການການອະນຸຍາດເພື່ອເຂົ້າເຖິງກ້ອງຖ່າຍຮູບ ແລະໄມໂຄຣໂຟນຂອງທ່ານເພື່ອໂທວິດີໂອ. +\n +\nກະລຸນາອະນຸຍາດໃຫ້ເຂົ້າເຖິງໃນpop-ups ຖັດໄປເພື່ອໃຫ້ສາມາດໂທອອກໄດ້. + ${app_name} ຕ້ອງການການອະນຸຍາດເພື່ອເຂົ້າເຖິງໄມໂຄຣໂຟນຂອງທ່ານເພື່ອປະຕິບັດການໂທສຽງ. + ຂໍ້ມູນ + ກຳລັງວາງສາຍ… + ທາງໄກບໍ່ສາມາດຮັບໄດ້. + ບໍ່ມີຄໍາຕອບ + ຜູ້ໃຊ້ທີ່ທ່ານໂທຫາບໍ່ຫວ່າງ. + ຜູ້ໃຊ້ບໍ່ຫວ່າງ + ທ່ານຖືສາຍການໂທ + %s ຖືສາຍ + ຖື + ປະຫວັດຫຍໍ້ + ໂທສຽງດ້ວຍ %s + ໂທວິດີໂອດ້ວຍ %s + ກຳລັງດຳເນີນການໂທດ້ວຍວິດີໂອ… + ກຳລັງດຳເນີນການໂທ… + ສາຍໂທເຂົ້າ + ສາຍເຂົ້າວິດີໂອ + + %d ບໍ່ໄດ້ຮັບສາຍວິດີໂອ + + + %d ສາຍບໍ່ໄດ້ຮັບ + + ສິ້ນສຸດການໂທ + ກຳລັງໂທເຂົ້ົ້າ… + ກຳລັງເຊື່ອມຕໍ່ການໂທ… + ໂທ + ເລືອກສຽງເອີ້ນເຂົ້າສໍາລັບການໂທ: + ສຽງເອີ້ນເຂົ້າ + ໃຊ້ສຽງເອີ້ນເຂົ້າ ${app_name} ເລີ່ມຕົ້ນສຳລັບສາຍໂທເຂົ້າ + ຂໍການຢືນຢັນກ່ອນເລີ່ມການໂທ + ປ້ອງກັນການໂທຫາໂດຍບັງເອີນ + ໂທ + ຂະຫນາດນ້ອຍ + ຂະຫນາດກາງ + ຂະຫນາດໃຫຍ່ + ຕົ້ນສະບັບ + + %d ການປ່ຽນແປງສະມາຊິກ + + ກະລຸນາເປີດ ${app_name} ໃນອຸປະກອນອື່ນທີ່ສາມາດຖອດລະຫັດຂໍ້ຄວາມໄດ້ເພື່ອໃຫ້ສາມາດສົ່ງລະຫັດໄປຫາລະບົບນີ້. + ຮ້ອງຂໍລະຫັດການເຂົ້າລະຫັດຄືນໃໝ່ຈາກລະບົບອື່ນຂອງທ່ານ. + ມີການຮ້ອງຂໍຫຼາຍເກີນໄປ + ບໍ່ມີ JSON ທີ່ຖືກຕ້ອງ + JSON ບໍ່ຖືກຕ້ອງ + ບໍ່ໄດ້ຮັບອະນຸຍາດ, ຂາດຂໍ້ມູນການພິສູດຢືນຢັນທີ່ຖືກຕ້ອງ + SSL ຜິດພາດ. + SSL ຜິດພາດ: ຂ້ມູນການເບິ່ງເຫັນຍັງບໍ່ໄດ້ຮັບການກວດສອບ. + ເລືອກ homeserver + ບໍ່ສາມາດເຂົ້າຫາ homeserver ທີ່ URL %s ໄດ້. ກະລຸນາກວດເບິ່ງລິ້ງຂອງທ່ານ ຫຼືເລືອກ homeserver ດ້ວຍຕົນເອງ. + ບໍ່ສາມາດເຂົ້າຫາ homeserver ຢູ່ URL ນີ້, ກະລຸນາກວດເບິ່ງ + ນີ້ບໍ່ແມ່ນທີ່ຢູ່ເຊີບເວີ Matrix ທີ່ຖືກຕ້ອງ + ກະລຸນາໃສ່ URL ທີ່ຖືກຕ້ອງ + ກະລຸນາກວດເບິ່ງ ແລະຍອມຮັບນະໂຍບາຍຂອງ homeserver ນີ້: + ການກວດສອບທີ່ຢູ່ອີເມວບໍ່ສຳເລັດ: ໃຫ້ແນ່ໃຈວ່າທ່ານໄດ້ກົດໃສ່ການເຊື່ອມຕໍ່ໃນອີເມວ + ຕ້ອງໃສ່ອິເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ. + homeserver ນີ້ຕ້ອງການໃຫ້ແນ່ໃຈວ່າທ່ານບໍ່ແມ່ນຫຸ່ນຍົນ + ລືມລະຫັດຜ່ານ\? + ເບີໂທລະສັບນີ້ຖືກກໍານົດໄວ້ແລ້ວ. + ທີ່ຢູ່ອີເມວນີ້ຖືກກຳນົດແລ້ວ. + ອັນນີ້ເບິ່ງຄືວ່າບໍ່ແມ່ນທີ່ຢູ່ອີເມວທີ່ຖືກຕ້ອງ + ຊື່ຜູ້ໃຊ້ ແລະ/ຫຼືລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ + ສົ່ງ + ເຂົ້າສູ່ລະບົບດ້ວຍການເຂົ້າສູ່ລະບົບຄັ້ງດຽວ + ເຂົ້າສູ່ລະບົບ + ຂໍອະໄພ, ບໍ່ພົບແອັບພລິເຄຊັນພາຍນອກເພື່ອເຮັດສຳເລັດການດຳເນິນການນີ້. + ໃນປັດຈຸບັນທ່ານບໍ່ໄດ້ເປີດໃຊ້ສະຕິກເກີແພັກເກັດໃດໆ. +\n +\nເພີ່ມບາງອັນດຽວນີ້ບໍ\? + ໃຊ້ເປັນຄ່າເລີ່ມຕົ້ນ ແລະ ຢ່າຖາມອີກ + ຖາມທຸກຄັ້ງ + ເອົາວິດີໂອ + ຖ່າຍຮູບ + ຖ່າຍຮູບ ຫຼືວິດີໂອ + ສົ່ງສະຕິກເກີ + ສົ່ງໄຟລ໌ + ເປີດ HD + ປິດ HD + ດ້ານຫລັງ + ດ້ານໜ້າ + ສະຫຼັບກ້ອງຖ່າຍຮູບ + ຊຸດຫູຟັງໄຮ້ສາຍ + ຊຸດຫູຟັງ + ລໍາໂພງ + ໂທລະສັບ + ເລືອກອຸປະກອນສຽງ + ການສ້າງການເຊື່ອມຕໍ່ແບບສົດໆບໍ່ສຳເລັດ. +\nກະລຸນາຮ້ອງຂໍໃຫ້ຜູ້ເບິ່ງແຍງ homeserver ຂອງທ່ານເພື່ອກໍານົດຄ່າເຊີບເວີ TURN ເພື່ອໃຫ້ການໂທເຮັດວຽກໄດ້ຢ່າງຫມັ້ນຄົງ. + ${app_name} ໂທລົ້ມເຫລວ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເລີ່ມການໂທດ້ວຍວິດີໂອ\? + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເລີ່ມການໂທສຽງ\? + ສົ່ງສຽງ + ລີ່ມການໂທວິດີໂອ + ເລີ່ມການໂທດ້ວຍສຽງ + ຊອກຫາ + API URL ຂອງສະຖານີ + Homeserver URL + ອອກຈາກລະບົບ + ຊື່ຜູ້ໃຊ້ + ເຂົ້າຮ່ວມຫ້ອງ + ຄວາມຄືບໜ້າ (%s%%) + ບໍ່ສາມາດສົ່ງລາຍງານຂໍ້ຜິດພາດໄດ້ (%s) + ໄດ້ສົ່ງລາຍງານຂໍ້ຜິດພາດໄປແລ້ວ + ສັ່ນເພື່ອລາຍງານຂໍ້ຜິດພາດ + ແອັບພລິເຄຊັນໄດ້ຂັດຂ້ອງໃນຄັ້ງທີ່ຜ່ານມາ. ທ່ານຕ້ອງການເປີດໜ້າຈໍລາຍງານການຂັດຂ້ອງບໍ\? + ເບິ່ງຄືວ່າທ່ານກຳລັງສັ່ນໂທລະສັບດ້ວຍຄວາມຫງຸດຫງິດ. ທ່ານຕ້ອງການເປີດໜ້າຈໍລາຍງານຂໍ້ຜິດພາດບໍ\? + ເພື່ອວິນິດໄສບັນຫາ, ບັນທຶກຈາກລູກຄ້ານີ້ຈະຖືກສົ່ງພ້ອມກັບບົດລາຍງານຂໍ້ຜິດພາດນີ້. ລາຍງານຂໍ້ຜິດພາດນີ້, ລວມທັງບັນທຶກ ແລະພາບໜ້າຈໍ, ຈະບໍ່ປາກົດໃຫ້ສາທາລະນະເຫັນໄດ້. ຖ້າທ່ານຕ້ອງການພຽງແຕ່ສົ່ງຂໍ້ຄວາມຂ້າງເທິງ, ກະລຸນາ ຢາກົດເລືອກ: + ອະທິບາຍບັນຫາຂອງທ່ານຢູ່ບ່ອນນີ້ + ຖ້າເປັນໄປໄດ້, ກະລຸນາຂຽນຄໍາອະທິບາຍເປັນພາສາອັງກິດ. + ກະລຸນາອະທິບາຍຂໍ້ບົກພ່ອງ. ທ່ານເຮັດຫຍັງລົງໄປ\?ທ່ານຄາດວ່າຈະມີຫຍັງເກີດຂຶ້ນ\? ທີ່ຈິງແລ້ວເກີດຫຍັງຂຶ້ນ\? + ລາຍງານຂໍ້ຜິດພາດ + ສົ່ງຮູບໜ້າຈໍ + ສົ່ງປະຫວັດການຮ້ອງຂໍການແບ່ງປັນທີ່ສໍາຄັນ + ສົ່ງບັນທຶກການຂັດຂ້ອງ + ສົ່ງບັນທຶກ + ພື້ນທີ່ + ຊຸມຊົນ + ສະແດງໃຫ້ເຫັນຫ້ອງທັງຫມົດໃນລະບົບຫ້ອງ, ລວມທັງຫ້ອງທີ່ມີເນື້ອຫາຊັດເຈນ. + ສະແດງຫ້ອງທີ່ມີເນື້ອຫາບໍ່ຈະແຈ້ງ + ຫ້ອງ + ບໍ່ມີຜົນໄດ້ຮັບອີກຕໍ່ໄປ + ບໍ່ມີຜົນໄດ້ຮັບ + ຕິດຕໍ່ Matrix ເທົ່ານັ້ນ + ການສົນທະນາ + ຫ້ອງແນະນຳ + ການແຈ້ງເຕືອນລະບົບ + ບູລິມະສິດຕໍ່າ + ເຊີນ + ກັ່ນຕອງຊື່ຫ້ອງ + ຫ້ອງ + ຄົນ + ລາຍການທີ່ມັກ + ການແຈ້ງເຕືອນ + ຄ່າໃໝ່ + ຄວາມສໍາເລັດ + ຜິດພາດ + ການຢືນຢັນ + ສຳເນົາລິ້ງໃສ່ກະທູ້ + ເບິ່ງຢູ່ໃນຫ້ອງ + ສຳເນົາໃສ່ຄລິບບອດແລ້ວ + ສຶກສາເພີ່ມເຕີມ + ຕັ້ງຄ່າຄືນໃໝ່ + ປິດ + ຫຼິ້ນ + ຕັດການເຊື່ອມຕໍ່ + ຖອນຄືນ + ປ່ຽນຊື່ + ລຶບ + ແບ່ງປັນ + ດາວໂຫຼດ + ວົງຢືມ + ສົ່ງ + ອອກໄປ + ບັນທຶກ + ຍົກເລີກ + ເຊີນ + ຫຼຸດລົງ + ບໍ່ສົນໃຈ + ຂ້າມ + ຍອມຮັບ + ປະຕິເສດ + ເຂົ້າຮ່ວມ + ລຶບອອກ + ປ່ຽນແປງ + ເຫັນດີ + ລອງໃຊ້ເບິ່ງ + ບໍ່ແມ່ນຕອນນີ້ + ປິດໃຊ້ງານ + ເປີດໃຊ້ + ຍົກເລີກການເຜີຍແຜ່ + ສະຫຼັບ + ເພີ່ມ + ສຳເນົາ + ປິດ + ເປີດ + ໝາຍວ່າອ່ານແລ້ວ + ຕອບກັບດ່ວນ + ໝາຍທັງໝົດວ່າອ່ານແລ້ວ + ເບິ່ງກະທູ້ + ໂທວິດີໂອ + ໂທສຽງ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກລະບົບ\? + ອກຈາກລະບົບ + ວາງສາຍ + ຫຼຸດລົງ + ຍອມຮັບ + ສຳເລັດແລ້ວ + ຫຼື + ລຶບວິດເຈັດບໍ່ສຳເລັດ + ເພີ່ມວິດເຈັດບໍ່ສຳເລັດ + ທ່ານບໍ່ສາມາດໂທຫາຕົວທ່ານເອງໄດ້, ລໍຖ້າໃຫ້ຜູ້ເຂົ້າຮ່ວມທີ່ຈະຮັບເອົາການເຊີນ + ທ່ານບໍ່ສາມາດໂທຫາຕົວທ່ານເອງໄດ້ + ກອງປະຊຸມໃຊ້ນະໂຍບາຍຄວາມປອດໄພ ແລະອະນຸຍາດ Jitsi. ຄົນທັງໝົດຢູ່ໃນຫ້ອງໃນປັດຈຸບັນຈະເຫັນຄຳເຊີນໃຫ້ເຂົ້າຮ່ວມ ໃນຂະນະທີ່ການປະຊຸມຂອງທ່ານກຳລັງເກີດຂຶ້ນ. + ເລີ່ມການປະຊຸມສຽງ + ເລີ່ມການປະຊຸມວິດີໂອ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເລີ່ມການໂທ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ໂທຢູ່ໃນຫ້ອງນີ້ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເລີ່ມການປະຊຸມທາງໂທລະສັບ + ທ່ານບໍ່ມີສິດໃນການເລີ່ມການໂທປະຊຸມຢູ່ໃນຫ້ອງນີ້ + ຂາດການອະນຸຍາດ + ເພື່ອສົ່ງຂໍ້ຄວາມສຽງ, ກະລຸນາອະນຸຍາດໄມໂຄຣໂຟນ. + ເພື່ອດໍາເນີນການນີ້, ກະລຸນາອະນຸຍາດກ້ອງຖ່າຍຮູບຈາກການຕັ້ງຄ່າລະບົບ. + ຂາດການອະນຸຍາດບາງຢ່າງເພື່ອດໍາເນີນການນີ້, ກະລຸນາໃຫ້ການອະນຸຍາດຈາກການຕັ້ງຄ່າລະບົບ. + ພື້ນທີ່ + ເລີ່ມການສົນທະນາ + ລາຍງານເນື້ອຫາ + ບໍ່ມີ + ເບິ່ງແຫຼ່ງທີ່ຖອດລະຫັດ + ເບິ່ງແຫຼ່ງ + ລິ້ງຖາວອນ + ຕໍ່ມາ + ຕົກລົງ + ກຳລັງໂຫລດ… + ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ຂອງທ່ານເວັ້ນເສຍແຕ່ທ່ານຈະສໍາຮອງລະຫັດຂອງທ່ານກ່ອນທີ່ຈະອອກຈາກລະບົບ. + ສຳຮອງ + ທ່ານແນ່ໃຈບໍ່\? + ກຳລັງສຳຮອງຂໍ້ມູນ… + ຂ້ອຍບໍ່ຕ້ອງການຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງຂ້ອຍ + ສຳຮອງຂໍ້ມູນກະເເຈຄວາມປອດໄພ ຄວນມີການເຄື່ອນໄຫວຢູ່ໃນທຸກລະບົບຂອງທ່ານເພື່ອຫຼີກເວັ້ນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານ. + ການສຳຮອງຂໍ້ມູນກະແຈກຳລັງດຳເນີນຢູ່. ຖ້າທ່ານອອກຈາກລະບົບດຽວນີ້, ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານ. + ທ່ານຈະສູນເສຍຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ຫາກທ່ານອອກຈາກລະບົບດຽວນີ້ + ໃຊ້ ກະເເຈສຳນອງ + ສໍາຮອງກະແຈ + ສົ່ງສະຕິກເກີ + ລາຍງານຂໍ້ຜິດພາດ + ການຕັ້ງຄ່າ + ການແຈ້ງເຕືອນສຽງງຽບ + ການແຈ້ງເຕືອນສຽງລົບກວນ + ກຳລັງຟັງການແຈ້ງເຕືອນ + ຟັງເຫດການ + ຮູບແບບຫົວຂໍ້ສີດໍາ + ຮູບແບບສີສັນມືດ + ຮູບແບບສີສັນແຈ້ງ + ຄ່າເລີ່ມຕົ້ນຂອງລະບົບ + ທ່ານໄດ້ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ (ລະບົບທີ່ບໍ່ສາມາດຮັບຮູ້ໄດ້ %1$s). + %1$s ເປີດໃຊ້ການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ (ລະບົບທີ່ບໍ່ສາມາດຮັບຮູ້ໄດ້ %2$s). + ທ່ານໄດ້ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. + %1$s ເປີດການເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. + ທ່ານໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ປ້ອງກັນບໍ່ໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມທີ່ນີ້. + %1$s ໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມທີ່ນີ້. + ທ່ານໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + %1$s ໄດ້ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມຫ້ອງ. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ຂອງຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ຂອງຫ້ອງນີ້. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ຫຼັກ ແລະທາງເລືອກສຳລັບຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ຕົ້ນຕໍ ແລະທາງເລືອກສຳລັບຫ້ອງນີ້. + ທ່ານໄດ້ປ່ຽນທີ່ຢູ່ທາງເລືອກສຳລັບຫ້ອງນີ້. + %1$s ໄດ້ປ່ຽນທີ່ຢູ່ທາງເລືອກສຳລັບຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບທີ່ຢູ່ສຳຮອງ %1$s ສໍາລັບຫ້ອງນີ້ອອກ. + + + %1$s ໄດ້ລຶບທີ່ຢູ່ສຳຮອງ %2$s ອອກຈາກຫ້ອງນີ້. + + + ທ່ານໄດ້ເພີ່ມທີ່ຢູ່ສຳຮອງ %1$s ສໍາລັບຫ້ອງນີ້. + + + %1$s ໄດ້ເພີ່ມທີ່ຢູ່ສຳຮອງ %2$s ສໍາລັບຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບທີ່ຢູ່ຫຼັກຂອງຫ້ອງນີ້ອອກ. + %1$s ລຶບທີ່ຢູ່ຫຼັກຂອງຫ້ອງນີ້ອອກ. + ທ່ານຕັ້ງຄ່າທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງນີ້ເປັນ %1$s. + %1$s ຕັ້ງຄ່າທີ່ຢູ່ຫຼັກສຳລັບຫ້ອງນີ້ເປັນ %2$s. + ທ່ານໄດ້ເພີ່ມ %1$s ແລະເອົາອອກ %2$sທີ່ຢູ່ຂອງຫ້ອງນີ້. + %1$s ເພີ່ມ %2$s ແລະ ເອົາອອກ %3$s ເປັນທີ່ຢູ່ຂອງຫ້ອງນີ້. + + ທ່ານໄດ້ລຶບອອກ %1$s ທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + + %1$s ລຶບອອກ %2$s ເປັນທີ່ຢູ່ຂອງຫ້ອງນີ້. + + + ທ່ານໄດ້ເພີ່ມ %1$s ເປັນທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + + %1$s ເພີ່ມ %2$s ເປັນທີ່ຢູ່ສຳລັບຫ້ອງນີ້. + + ທ່ານໄດ້ຖອນຄຳເຊີນຂອງ %1$s ແລ້ວ. ເຫດຜົນ: %2$s + %1$s ຖອນຄຳເຊີນຂອງ %2$s. ເຫດຜົນ: %3$s + ທ່ານຍອມຮັບຄຳເຊີນສຳລັບ %1$s. ເຫດຜົນ: %2$s + %1$s ຍອມຮັບຄຳເຊີນສຳລັບ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ຖືກຫ້າມ %1$s. ເຫດຜົນ: %2$s + %1$s ຖືກຫ້າມ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ຍົກເລີກການຫ້າມ %1$s. ເຫດຜົນ: %2$s + %1$s ຍົກເລີກການຫ້າມ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ລຶບ %1$s ອອກແລ້ວ. ເຫດຜົນ: %2$s + %1$s ລຶບອອກ %2$s. ເຫດຜົນ: %3$s + ທ່ານໄດ້ປະຕິເສດການເຊີນ. ເຫດຜົນ: %1$s + %1$s ປະຕິເສດຄຳເຊີນ. ເຫດຜົນ: %2$s + ທ່ານອອກໄປ. ເຫດຜົນ: %1$s + ອອກ %1$s. ເຫດຜົນ: %2$s + ທ່ານໄດ້ອອກຈາກຫ້ອງ. ເຫດຜົນ: %1$s + %1$s ອອກຈາກຫ້ອງ. ເຫດຜົນ: %2$s + ທ່ານເຂົ້າຮ່ວມ. ເຫດຜົນ: %1$s + %1$s ເຂົ້າຮ່ວມ. ເຫດຜົນ: %2$s + ທ່ານໄດ້ເຂົ້າຮ່ວມຫ້ອງ. ເຫດຜົນ: %1$s + %1$s ເຂົ້າຮ່ວມຫ້ອງ. ເຫດຜົນ: %2$s + %1$s ເຊີນທ່ານ. ເຫດຜົນ: %2$s + ທ່ານໄດ້ຮັບເຊີນ %1$s. ເຫດຜົນ: %2$s + %1$s ເຊີນ %2$s. ເຫດຜົນ: %3$s + ການເຊື້ອເຊີນຂອງທ່ານ. ເຫດຜົນ: %1$s + ຄຳເຊີນຂອງ %1$s. ເຫດຜົນ: %2$s + ກຳລັງສົ່ງຂໍ້ຄວາມ… + ຂໍ້ຄວາມຖືກສົ່ງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຂໍ້ມູນບັນຊີ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nຊຸມຊົນນໍາເຂົ້າ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຫ້ອງຊ້າຍ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງນຳເຂົ້າຫ້ອງຮັບເຊີນ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງໂຫຼດບົດສົນທະນາຂອງທ່ານ +\nຖ້າທ່ານໄດ້ເຂົ້າຮ່ວມຫຼາຍຫ້ອງ, ນີ້ອາດຈະໃຊ້ເວລາໄລຍະຫນຶ່ງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າຫ້ອງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nການນໍາເຂົ້າ crypto + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງນຳເຂົ້າບັນຊີ… + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງດາວໂຫຼດຂໍ້ມູນ… + ການຊິງຄ໌ເບື້ອງຕົ້ນ: +\nກຳລັງລໍຖ້າການຕອບສະໜອງຂອງເຊີບເວີ… + ຫ້ອງຫວ່າງ (ແມ່ນ %s) + ຫ້ອງຫວ່າງ + + %1$s, %2$s, %3$s ແລະ %4$d ອື່ນໆ + + %1$s, %2$s, %3$s ແລະ %4$s + %1$s, %2$s ແລະ %3$s + %1$s ແລະ %2$s + ເຊີນຫ້ອງ + ເບີໂທລະສັບ + ທີ່ຢູ່ອີເມວ + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເຂົ້າຮ່ວມຫ້ອງນີ້ + Matrix ຜິດພາດ + ບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້ + ອຸປະກອນຂອງຜູ້ສົ່ງບໍ່ໄດ້ສົ່ງກະແຈໃຫ້ພວກເຮົາສໍາລັບຂໍ້ຄວາມນີ້. + ** ບໍ່ສາມາດຖອດລະຫັດໄດ້: %s ** + %1$s ຈາກ %2$s ຫາ %3$s + %1$s ໄດ້ປ່ຽນລະດັບພະລັງງານຂອງ %2$s. + ທ່ານໄດ້ປ່ຽນລະດັບພະລັງງານຂອງ %1$s. + ກຳນົດເອງ + ກຳນົດເອງ (%1$d) + ຜູ້ເບີ່ງເເຍງລະບົບ + ຄ່າເລີ່ມຕົ້ນ + ຜູ້ຄວບຄຸມ + ທ່ານໄດ້ແກ້ໄຂວິດເຈັດ %1$s + %1$s ແກ້ໄຂວິດເຈັດ %2$s + ທ່ານລຶບວິດເຈັດ %1$s ອອກແລ້ວ + %1$s ລຶບວິດເຈັດ %2$s ອອກແລ້ວ + ທ່ານໄດ້ເພີ່ມວິດເຈັດ %1$s ແລ້ວ + %1$s ເພີ່ມວິດເຈັດ %2$s ແລ້ວ + ທ່ານຍອມຮັບຄຳເຊີນສຳລັບ %1$s + %1$s ຍອມຮັບຄຳເຊີນສຳລັບ %2$s + ທ່ານໄດ້ຖອນຄຳເຊີນສຳລັບ %1$s ແລ້ວ + %1$s ຖອນຄຳເຊີນສຳລັບ %2$s + ທ່ານໄດ້ຖອນຄຳເຊີນສຳລັບ %1$s ເຂົ້າຮ່ວມຫ້ອງແລ້ວ + %1$s ຖອນຄຳເຊີນສຳລັບ %2$s ເຂົ້າຮ່ວມຫ້ອງ + ທ່ານໄດ້ຮັບເຊີນ %1$s + %1$s ເຊີນ %2$s + ທ່ານໄດ້ສົ່ງຄຳເຊີນໄປຫາ %1$s ເພື່ອເຂົ້າຮ່ວມຫ້ອງ + %1$s ສົ່ງຄຳເຊີນໃຫ້ %2$s ເຂົ້າຮ່ວມຫ້ອງ + ທ່ານໄດ້ລຶບຮູບແທນຕົວຂອງຫ້ອງອອກແລ້ວ + %1$s ລຶບຮູບແທນຕົວຂອງຫ້ອງອອກແລ້ວ + ທ່ານໄດ້ລຶບຫົວຂໍ້ຫ້ອງອອກ + %1$s ລຶບຫົວຂໍ້ຫ້ອງອອກ + ທ່ານໄດ້ລຶບຊື່ຫ້ອງອອກ + %1$s ລຶບຊື່ຫ້ອງອອກ + (ຮູບຕົວແທນຖືກປ່ຽນແປງເຊັ່ນດຽວກັນ) + 🎉 ເຊີບເວີທັງໝົດຖືກຫ້າມບໍ່ໃຫ້ເຂົ້າຮ່ວມ! ຫ້ອງນີ້ບໍ່ສາມາດໃຊ້ໄດ້ອີກຕໍ່ໄປ. + ບໍ່ມີການປ່ຽນແປງ. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ຖືກຫ້າມແລ້ວ. + ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ໄດ້ຖືກອະນຸຍາດແລ້ວ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກລຶບອອກຈາກລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ. + • ຕອນນີ້ເຊີບເວີທີ່ກົງກັບ %s ໄດ້ຖືກອະນຸຍາດແລ້ວ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກເອົາອອກຈາກລາຍການຫ້າມແລ້ວ. + • ຕອນນີ້ເຊີບເວີທີ່ກົງກັບ %s ຖືກຫ້າມແລ້ວ. + + %d ປ່ຽນແປງເຊີບເວີ ACL + + ທ່ານໄດ້ປ່ຽນເຊີບເວີ ACLs ສໍາລັບຫ້ອງນີ້. + %s ໄດ້ປ່ຽນເຊີບເວີ ACLs ສໍາລັບຫ້ອງນີ້. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ຖືກຫ້າມ. + • ເຊີບເວີທີ່ກົງກັບຕົວອັກສອນ IP ແມ່ນອະນຸຍາດ. + • ເຊີບເວີທີ່ກົງກັບ %s ໄດ້ຮັບອະນຸຍາດ. + • ເຊີບເວີທີ່ກົງກັບ %s ຖືກຫ້າມ. + ທ່ານຕັ້ງ ACLs ເຊີບເວີສໍາລັບຫ້ອງນີ້. + %s ຕັ້ງ ACL ຂອງເຊີບເວີສຳລັບຫ້ອງນີ້. + ທ່ານໄດ້ຍົກລະດັບຢູ່ທີ່ນີ້. + %s ຍົກລະດັບຢູ່ບ່ອນນີ້ແລ້ວ. + ທ່ານໄດ້ຍົກລະດັບຫ້ອງນີ້. + %s ໄດ້ຍົກລະດັບຫ້ອງນີ້ແລ້ວ. + ຜູ້ໃດຜູ້ຫນຶ່ງ. + ສະມາຊິກຫ້ອງທັງຫມົດ. + ສະມາຊິກຫ້ອງທັງຫມົດ, ຈາກຈຸດທີ່ເຂົາເຈົ້າເຂົ້າຮ່ວມ. + ສະມາຊິກຫ້ອງທັງຫມົດ, ຈາກຈຸດທີ່ເຂົາເຈົ້າໄດ້ຖືກເຊີນ. + ທ່ານໄດ້ສ້າງຂໍ້ຄວາມໃນອະນາຄົດເຫັນໄດ້ຕໍ່ %1$s + %1$s ໄດ້ສ້າງຂໍ້ຄວາມອະນາຄົດເຫັນໄດ້ໂດຍ %2$s + ທ່ານໄດ້ສ້າງປະຫວັດຫ້ອງໃນອະນາຄົດໃຫ້ ເຫັນໄດ້%1$s + %1$s ສ້າງປະຫວັດຫ້ອງໃນອະນາຄົດໃຫ້ ເຫັນໄດ້%2$s + ທ່ານສິ້ນສຸດການໂທ. + %s ວາງສາຍ. + ທ່ານໄດ້ຮັບສາຍ. + %s ຮັບສາຍ. + ທ່ານໄດ້ສົ່ງຂໍ້ມູນເພື່ອຕັ້ງຄ່າການໂທ. + %s ສົ່ງຂໍ້ມູນເພື່ອຕັ້ງຄ່າການໂທ. + ທ່ານໄດ້ໂທດ້ວຍສຽງ. + %s ໂທອອກ. + ທ່ານໄດ້ໂທວິດີໂອ. + %s ໂທດ້ວຍວິດີໂອ. + ທ່ານໄດ້ປ່ຽນຊື່ຫ້ອງເປັນ: %1$s + %1$s ໄດ້ປ່ຽນຊື່ຫ້ອງເປັນ: %2$s + ທ່ານໄດ້ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + %1$s ໄດ້ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງ + ທ່ານໄດ້ປ່ຽນຫົວຂໍ້ເປັນ: %1$s + %1$s ໄດ້ປ່ຽນຫົວຂໍ້ເປັນ: %2$s + ທ່ານໄດ້ລຶບການສະເເດງຊື່ຂອງເຈົ້າອອກ (ມັນແມ່ນ %1$s) + %1$s ລຶບການສະເເດງຊື່ຂອງເຂົາເຈົ້າອອກ (ມັນແມ່ນ %2$s) + ທ່ານໄດ້ປ່ຽນການສະເເດງຊື່ຂອງທ່ານຈາກ %1$s ເປັນ %2$s + %1$s ໄດ້ປ່ຽນການສະແດງຊື່ຂອງເຂົາເຈົ້າຈາກ %2$s ເປັນ %3$s + ທ່ານກຳນົດການສະເເດງຊື່ຊື່ຂອງທ່ານເປັນ %1$s + %1$s ກຳນົດນົດການສະແດງຊື່ຂອງເຂົາເຈົ້າເປັນ %2$s + ທ່ານໄດ້ປ່ຽນຮູບແທນຕົວຂອງເຈົ້າແລ້ວ + %1$s ໄດ້ປ່ຽນຮູບແທນຕົວຂອງເຂົາເຈົ້າແລ້ວ + ທ່ານໄດ້ຖອນຄຳເຊີນຂອງ %1$s ແລ້ວ + %1$s ຖອນຄຳເຊີນຂອງ %2$s + ທ່ານໄດ້ຫ້າມ%1$s + %1$s ໄດ້ຫ້າມ %2$s + ທ່ານໄດ້ຍົກເລີກການຫ້າມ %1$s + %1$s ໄດ້ຍົກເລີກການຫ້າມ %2$s + ທ່ານໄດ້ລຶບ %1$s ອອກແລ້ວ + %1$sໄດ້ ລຶບອອກ %2$s + ທ່ານໄດ້ປະຕິເສດການເຊີນ + %1$s ໄດ້ປະຕິເສດຄຳເຊີນ + ທ່ານໄດ້ອອກຈາກຫ້ອງ + %1$s ໄດ້ອອກຈາກຫ້ອງ + ທ່ານໄດ້ອອກຈາກຫ້ອງ + %1$s ອອກຈາກຫ້ອງ + ທ່ານໄດ້ເຂົ້າຮ່ວມ + %1$s ໄດ້ເຂົ້າຮ່ວມ + ທ່ານໄດ້ເຂົ້າຮ່ວມຫ້ອງ + %1$s ໄດ້ເຂົ້າຮ່ວມຫ້ອງ + %1$s ໄດ້ເຊີນທ່ານ + ທ່ານໄດ້ເຊີນ %1$s + %1$s ໄດ້ເຊີນ %2$s + ທ່ານໄດ້ຕັ້ງກະທູ້ສົນທະນາ + %1$s ໄດ້ຕັ້ງກະທູ້ສົນທະນາ + ທ່ານໄດ້ສ້າງຫ້ອງ + %1$s ໄດ້ສ້າງຫ້ອງ + ບັດເຊີນຂອງທ່ານ + ການເຊີນຂອງ %s + ບໍ່ພົບການແກ້ໄຂ + ການແກ້ໄຂຂໍ້ຄວາມ + (ດັດແກ້ແລ້ວ) + ໄຟລ໌ %1$s ຖືກດາວໂຫຼດແລ້ວ! + ກຳລັງບີບອັດວິດີໂອ %d%% + ກະທູ້ກຳລັງດຳເນີນຢູ່ພ້ອມກັບຄຸນສົມບັດໃໝ່ທີ່ກຳລັງຈະມາຮອດ ເຊັ່ນ: ການແຈ້ງເຕືອນປັບປຸງໃຫ້ດີຂຶ້ນ. ພວກເຮົາຢາກໄດ້ຍິນຄໍາຕິຊົມຂອງທ່ານ! + app_display_name: + ກຳລັງບີບອັດຮູບ… + ກຳລັງສົ່ງໄຟລ໌ (%1$s / %2$s) + ກຳລັງເຂົ້າລະຫັດໄຟລ໌… + ກຳລັງສົ່ງຮູບຕົວຢ່າງ (%1$s / %2$s) + ກຳລັງເຂົ້າລະຫັດຮູບຕົວຢ່າງ… + ກຳລັງລໍຖ້າ… + ຂໍ້ຄວາມໂດຍກົງ + ສະແດງປະຫວັດຄົບຖ້ວນຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ + ສະແດງເຫດການທີ່ເຊື່ອງໄວ້ໃນທາມລາຍ + ເບຕ້າ + ຄວາມຄິດເຫັນຂອງກະທູ້ Beta + ໃຫ້ຄໍາຄິດເຫັນ + ໃຫ້ຄໍາຄິດເຫັນ + ສົ່ງຄຳຕິຊົມບໍ່ສຳເລັດ (%s) + ຂໍຂອບໃຈ, ຄໍາຄຶດຄໍາເຫັນຂອງທ່ານໄດ້ຖືກສົ່ງສໍາເລັດແລ້ວ + ທ່ານສາມາດຕິດຕໍ່ຂ້ອຍໄດ້ ຖ້າທ່ານມີຄໍາຖາມຕິດຕາມ + ທ່ານກຳລັງໃຊ້ພື້ນທີ່ເວີຊັ້ນເບຕ້າ. ຄໍາຕິຊົມຂອງທ່ານຈະຊ່ວຍແຈ້ງໃຫ້ເວີຊັ້ນຕໍ່ໄປຊາບ. ເເພລັດຟອມແລະຊື່ຜູ້ໃຊ້ຂອງທ່ານຈະຖືກບັນທຶກໄວ້ເພື່ອຊ່ວຍໃຫ້ພວກເຮົາໃຊ້ຄໍາຕິຊົມຂອງທ່ານເທົ່າທີ່ພວກເຮົາສາມາດເຮັດໄດ້. + ຄໍາຕິຊົມ + ຄຳຕິຊົມຂອງພື້ນທີ່ + ເບຕ້າ + ສົ່ງຄຳແນະນຳບໍ່ສຳເລັດ (%s) + ຂອບໃຈ, ຄຳແນະນຳໄດ້ສົ່ງສຳເລັດແລ້ວ + ອະທິບາຍຄໍາແນະນໍາຂອງທ່ານທີ່ນີ້ + ກະລຸນາຂຽນຄໍາແນະນໍາຂອງທ່ານຂ້າງລຸ່ມນີ້. + ໃຫ້ຄໍາແນະນໍາ + ລົງທະບຽນ token + ການຕັ້ງຄ່າລະບົບ + ເວີຊັ້ນ + ຂໍຄວາມຊ່ວຍເຫຼືອໃນການນຳໃຊ້ ${app_name} + ຊ່ວຍເຫຼືອ ແລະ ສະຫນັບສະຫນູນ + ຊ່ວຍເຫຼືອ + ກົດໝາຍ + ຊ່ວຍເຫຼືອ & ກ່ຽວກັບ + ສຽງ & ວິດີໂອ + ຮູບແບບ: + Url: + session_name: + push_key: + app_id: + ບໍ່ມີປະຕູທາງpushທີ່ລົງທະບຽນ + ບໍ່ໄດ້ກໍານົດກົດລະບຽບການຊຸກຍູ້ + ກົດລະບຽບການຊຸກຍູ້ + ຄວາມປອດໄພ & ຄວາມເປັນສ່ວນຕົວ + ຄວາມມັກ + ທົ່ວໄປ + ທ່ານກຳລັງເບິ່ງກະທູ້ນີ້ຢູ່! + ທ່ານກຳລັງເບິ່ງຫ້ອງນີ້ຢູ່ແລ້ວ! + ແຈ້ງການອື່ນໆຂອງພາກສ່ວນທີສາມ + ສະບັບ Matrix SDK + ນຳເຂົ້າກະແຈ e2e ຈາກໄຟລ໌ \"%1$s\". + ເກີດຄວາມຜິດພາດໃນການຮັບເອົາຂໍ້ມູນສຳຮອງກະແຈ + ເກີດຄວາມຜິດພາດໃນການຮັບຂໍ້ມູນໜ້າເຊື່ອຖື + ຫ້ອງໄດ້ຖືກສ້າງຂື້ນ, ແຕ່ການເຊີນບາງອັນບໍ່ໄດ້ຖືກສົ່ງໄປດ້ວຍເຫດຜົນຕໍ່ໄປນີ້:%s + ທຸກຄົນຈະສາມາດເຂົ້າຮ່ວມຫ້ອງນີ້ໄດ້ + ສາທາລະນະ + ການຕັ້ງຄ່າຫ້ອງ + ຫົວຂໍ້ + ຫົວຂໍ້ຫ້ອງ (ທາງເລືອກ) + ຊື່ + ຊື່ຫ້ອງ + ສ້າງ + ຂໍ້ຄວາມໂດຍກົງ + ຫ້ອງ + ຫ້ອງນີ້ບໍ່ສາມາດເບິ່ງຕົວຢ່າງໄດ້. ທ່ານຕ້ອງການເຂົ້າຮ່ວມບໍ\? + ຫ້ອງນີ້ບໍ່ສາມາດເຂົ້າເຖິງໄດ້ໃນເວລານີ້. +\nລອງໃໝ່ໃນພາຍຫຼັງ, ຫຼືຖາມຜູ້ເບິ່ງແຍງຫ້ອງເພື່ອກວດເບິ່ງວ່າທ່ານມີການເຂົ້າເຖິງຫຼືບໍ່. + ຫ້ອງນີ້ບໍ່ສາມາດເບິ່ງຕົວຢ່າງໄດ້ + ຊຸມຊົນທັງໝົດ + ກະລຸນາລໍຖ້າ… + ປ່ຽນເຄືອຂ່າຍ + ບໍ່ມີເຄືອຂ່າຍ. ກະລຸນາກວດເບິ່ງການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງທ່ານ. + ສ້າງພື້ນທີ່ໃຫມ່ + ສ້າງຫ້ອງໃຫມ່ + ເຫດການບໍ່ຖືກຕ້ອງ, ບໍ່ສາມາດສະແດງໄດ້ + ຄວບຄຸມເຫດການໂດຍຜູ້ເບິ່ງແຍງຫ້ອງ + ເຫດການຖືກລຶບໂດຍຜູ້ໃຊ້ + ສະແດງຕົວຍຶດສໍາລັບຂໍ້ຄວາມທີ່ຖືກລົບອອກ + ສະແດງຂໍ້ຄວາມທີ່ຖືກລືບອອກ + ຂໍ້ຄວາມຖືກລຶບ + ການໂຕ້ຕອບ + ເບິ່ງການໂຕ້ຕອບ + ເພີ່ມປະຕິກິລິຍາ + ປະຕິກິລິຍາ + ຫ້ອງຂອງທ່ານຈະຖືກສະແດງຢູ່ທີ່ນີ້. ແຕະ + ຂວາລຸ່ມເພື່ອຊອກຫາອັນທີ່ມີຢູ່ແລ້ວ ຫຼື ເລີ່ມຕົ້ນໂດຍທ່ານເອງ. + ຫ້ອງ + ການສົນທະນາທາງຂໍ້ຄວາມໂດຍກົງຂອງທ່ານຈະຖືກສະແດງຢູ່ທີ່ນີ້. ແຕະ + ຂວາລຸ່ມເພື່ອເລີ່ມຕົ້ນ. + ການສົນທະນາ + ປຸ່ມEnter ຂອງແປ້ນພິມຈະສົ່ງຂໍ້ຄວາມແທນການເພີ່ມຕົວແບ່ງແຖວ + ສົ່ງຂໍ້ຄວາມດ້ວຍການກົດ enter + ເບິ່ງຕົວຢ່າງສື່ກ່ອນສົ່ງ + ສັ່ນເມື່ອກ່າວເຖິງຜູ້ໃຊ້ + ລວມທັງຮູບແທນຕົວ ແລະ ການປ່ຽນແປງການສະແດງຊື່. + ສະແດງບັນຊີ + ການເຊີນ, ລຶບ, ແລະການຫ້າມແມ່ນບໍ່ມີຜົນກະທົບ. + ສະແດງການເຂົ້າຮ່ວມ ແລະ ອອກຈາກເຫດການ + ໃຊ້ຄໍາສັ່ງ /confetti ຫຼືສົ່ງຂໍ້ຄວາມທີ່ມີ ❄️ ຫຼື 🎉 + ສະແດງຜົນການສົນທະນາ + ໃບຮັບຮອງໄດ້ປ່ຽນຈາກອັນທີ່ເຊື່ອຖືໄດ້. ນີ້ແມ່ນຜິດປົກກະຕິ. ຂໍແນະນຳໃຫ້ທ່ານຢ່າຍອມຮັບໃບຮັບຮອງໃໝ່ນີ້. + ທ່ານບໍ່ມີຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ເຊີນໂດຍ %s + ສົ່ງຄຳເຊີນໃຫ້ທ່ານ + ລອງໃໝ່ + ເບິ່ງຢູ່ໃນຫ້ອງ + ຕອບໃນກະທູ້ + ຕອບ + ແກ້ໄຂ + ເບິ່ງຄືວ່າທ່ານກຳລັງພະຍາຍາມເຊື່ອມຕໍ່ຫາ homeserver ອື່ນ. ທ່ານຕ້ອງການອອກຈາກລະບົບບໍ\? + ທ່ານຍັງບໍ່ໄດ້ໃຊ້ເຊີບເວີໃດໆ + ຄວາມຜິດພາດທີ່ບໍ່ຮູ້ຈັກ + %s ຕ້ອງການກວດສອບລະບົບຂອງທ່ານ + ການຮ້ອງຂໍການຢັ້ງຢືນ + ໄດ້ແລ້ວ + ຢືນຢັນແລ້ວ! + ລາຍເຊັນ + ສູດການຄິດໄລ່ + ເວີຊັ້ນ + + ກຳລັງສຳຮອງຂໍ້ມູນ %d ກະແຈ… + + ກະແຈທັງໝົດຖືກສຳຮອງໄວ້ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ + ກຳລັງສຳຮອງຂໍ້ມູນກະແຈຂອງທ່ານ. ອັນນີ້ອາດຈະໃຊ້ເວລາຫຼາຍນາທີ… + ຈັດການການ ສຳຮອງຂໍ້ມູນກະເເຈ + ລະຫັດຂໍ້ຄວາມໃໝ່ທີ່ປອດໄພ + ການສຳຮອງຂໍ້ມູນກະເເຈ + ຂໍ້ຄວາມເຂົ້າລະຫັດຈະບໍ່ສູນເສຍ + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ ແລະຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດ + ການສໍາຮອງທີ່ປອດໄພ + ລຶບການເຂົ້າລະຫັດສຳຮອງຂອງທ່ານອອກຈາກເຊີບເວີບໍ\? ທ່ານຈະບໍ່ສາມາດໃຊ້ລະຫັດກູ້ຄືນຂອງທ່ານເພື່ອອ່ານປະຫວັດຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດໄດ້ອີກຕໍ່ໄປ. + ລຶບການສຳຮອງຂໍ້ມູນ + ກຳລັງກວດສອບສະຖານະການສຳຮອງ + ກຳລັງລຶບການສຳຮອງຂໍ້ມູນ… + ການສຳຮອງຂໍ້ມູນກະເເຈ ໃນລະບົບນີ້, ກູ້ຂໍ້ມູນດ້ວຍປະໂຫຍກຄວາມປອດໄພ ຫຼື ກູ້ຂໍ້ມູນກະແຈຂອງທ່ານດຽວນີ້. + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນບໍ່ໄດ້ຮັບການຢັ້ງຢືນທີ່ຖືກຕ້ອງ %s + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນບໍ່ຖືກຕ້ອງໄດ້ຢືນຢັນຈາກລະບົບແລ້ວ %s + ການສຳຮອງຂໍ້ມູນມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບທີ່ບໍ່ໄດ້ຜ່ານການຢືນຢັນ %s + ການສຳຮອງຂໍ້ມູນ ມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບທີ່ຢືນຢັນແລ້ວ %s. + ການສຳຮອງຂໍ້ມູນ ມີລາຍເຊັນທີ່ຖືກຕ້ອງຈາກລະບົບນີ້. + ກູ້ຂໍ້ມູນມີລາຍເຊັນຈາກລະບົບທີ່ບໍ່ຮູ້ຈັກກັບ ID %s. + ກະແຈຂອງທ່ານບໍ່ໄດ້ສຳຮອງຂໍ້ມູນຈາກລະບົບນີ້. + ການສຳຮອງຂໍ້ມູນກະເເຈບໍ່ໄດ້ໃຊ້ງານຢູ່ໃນລະບົບນີ້. + ການສຳຮອງຂໍ້ມູນກະເເຈ ໄດ້ຖືກຕັ້ງຄ່າຢ່າງຖືກຕ້ອງສໍາລັບລະບົບນີ້. + ລຶບການສຳຮອງຂໍ້ມູນ + ກູ້ຄືນຈາກການສໍາຮອງຂໍ້ມູນ + ການໂຫຼດລະຫັດການກູ້ຄືນຫຼ້າສຸດບໍ່ສຳເລັດ(%s). + + %d ກະແຈໃໝ່ໄດ້ຖືກເພີ່ມໃສ່ໃນລະບົບນີ້. + + + ກູ້ຄືນການສຳຮອງຂໍ້ມູນດ້ວຍກະແຈ %d ແລ້ວ. + + ກູ້ຂໍ້ມູນຄືນມາ %s ! + ການສຳຮອງຂໍ້ມູນບໍ່ສາມາດຖອດລະຫັດດ້ວຍກະແຈການກູ້ຂໍ້ມູນນີ້ໄດ້: ກະລຸນາກວດສອບວ່າທ່ານໃສ່ລະຫັດການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ. + ກະລຸນາໃສ່ລະຫັດການກູ້ຂໍ້ມູນ + ປົດລັອກປະຫວັດ + ກຳລັງນຳເຂົ້າກະແຈ… + ກຳລັງດາວໂຫຼດກະແຈ… + ກຳລັງກູ້ຄືນລະຫັດຄອມພິວເຕີ… + ການກູ້ຄືນການສໍາຮອງຂໍ້ມູນ: + ການສຳຮອງຂໍ້ມູນບໍ່ສາມາດຖອດລະຫັດດ້ວຍປະໂຫຍກຄວາມປອດໄພນີ້: ກະລຸນາກວດສອບວ່າທ່ານໃສ່ລະຫັດຜ່ານການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ. + ກະແຈການກູ້ຄືນຂອງທ່ານສູນເສຍ\? ທ່ານສາມາດຕັ້ງຄ່າອັນໃໝ່ໄດ້ໃນການຕັ້ງຄ່າ. + ກະລຸນາໃສ່ລະຫັດການກູ້ຄຶນ + ໃຊ້ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານເພື່ອປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ບໍ່ຮູ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນຂອງທ່ານ, ທ່ານສາມາດ %s ໄດ້. + ໃຊ້ລະຫັດການກູ້ຄືນຂອງທ່ານ + ໃຊ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນຂອງທ່ານເພື່ອປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ກຳລັງດຶງເອົາເວີຊັນສຳຮອງ… + ທ່ານອາດຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມຂອງທ່ານຖ້າທ່ານອອກຈາກລະບົບຫຼືສູນເສຍອຸປະກອນນີ້. + ທ່ານແນ່ໃຈບໍ່\? + ຄວາມຜິດພາດທີ່ບໍ່ຄາດຄິດ + ລະຫັດການກູ້ຄືນ + ພວກເຮົາ <b>ບໍ່</b> ແບ່ງປັນຂໍ້ມູນໃຫ້ພາກສ່ວນທີສາມ + ພວກເຮົາ <b>ບໍ່</b> ບັນທຶກ ຫຼື ບັນທຶກຂໍ້ມູນໜ້າປົກບັນຊີໃດໆ + ທີ່ນີ້ + ຊ່ວຍພວກເຮົາລະບຸບັນຫາ ແລະປັບປຸງ ${app_name} ໂດຍການແບ່ງປັນຂໍ້ມູນການນຳໃຊ້ທີ່ບໍ່ເປີດເຜີຍຊື່. ເພື່ອເຂົ້າໃຈວິທີໃຊ້ຫຼາຍອຸປະກອນ, ພວກເຮົາຈະສ້າງຕົວກຳນົດແບບສຸ່ມ, ແບ່ງປັນໂດຍອຸປະກອນຂອງທ່ານ. +\n +\nທ່ານສາມາດອ່ານເງື່ອນໄຂທັງໝົດຂອງພວກເຮົາໄດ້ %s. + ຊ່ວຍປັບປຸງ ${app_name} + ${app_name} ລວບລວມການວິເຄາະທີ່ບໍ່ເປີດເຜີຍຊື່ເພື່ອໃຫ້ພວກເຮົາປັບປຸງແອັບພລິເຄຊັນ. + ສົ່ງຂໍ້ມູນການວິເຄາະ + ການວິເຄາະ + ຈັດການການຕັ້ງຄ່າການຄົ້ນພົບຂອງທ່ານ. + ການຄົ້ນພົບ + ປິດການນຳໃຊ້ບັນຊີຂອງຂ້ອຍ + ປິດການນຳໃຊ້ບັນຊີ + ນີ້ຈະປ່ຽນແທນ ຫຼືປະໂຫຍກປັດຈຸບັນຂອງທ່ານ. + ສ້າງກະແຈຄວາມປອດໄພໃໝ່ ຫຼືຕັ້ງປະໂຫຍກຄວາມປອດໄພໃໝ່ສຳລັບການສຳຮອງຂໍ້ມູນທີ່ມີຢູ່ແລ້ວຂອງທ່ານ. + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດໂດຍການສໍາຮອງການເຂົ້າລະຫັດຢູ່ໃນເຊີບເວິຂອງທ່ານ. + ຕັ້ງຄ່າໃນອຸປະກອນນີ້ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພຄືນໃໝ່ + ຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ + ການສໍາຮອງທີ່ປອດໄພ + ເພີ່ມປຸ່ມໃສ່ຕົວຂຽນຂໍ້ຄວາມເພື່ອເປີດແປ້ນພິມ emoji + ສະແດງແປ້ນພິມ emoji + ການສ້າງລະຫັດການກຸ້ຂໍ້ມູນໂດຍໃຊ້ປະໂຫຍກລະຫັດຜ່ານ, ຂະບວນການນີ້ສາມາດໃຊ້ເວລາຫຼາຍວິນາທີ. + ແບ່ງປັນລະຫັດການກູ້ຂໍ້ມູນກັບ… + ກະລຸນາສ້າງສໍາເນົາ + ຢຸດ + ແທນທີ່ + ເບິ່ງຄືວ່າທ່ານມີການຕັ້ງຄ່າການສຳຮອງລະຫັດຈາກລະບົບອື່ນຢູ່ກ່ອນແລ້ວ. ທ່ານຕ້ອງການແທນທີ່ມັນກັບອັນທີ່ທ່ານກຳລັງສ້າງບໍ\? + ມີຂໍ້ມູນສຳຮອງຢູ່ໃນເຊີບເວີຂອງທ່ານຢູ່ກ່ອນແລ້ວ + ລະຫັດການກູ້ຂໍ້ມູນໄດ້ຖືກບັນທຶກໄວ້. + ບັນທຶກເປັນໄຟລ໌ + ແບ່ງປັນ + ບັນທຶກລະຫັດການກູ້ຂໍ້ມູນ + ຂ້ອຍໄດ້ສ້າງສຳເນົາ + ສຳເລັດແລ້ວ + ຮັກສາກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພຫຼາຍ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ (ຫຼືບ່ອນປອດໄພ) + ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານເປັນຕາໜ່າງຄວາມປອດໄພ - ທ່ານສາມາດໃຊ້ມັນເພື່ອກູ້ການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຂອງທ່ານຄືນມາໄດ້ ຖ້າທ່ານລືມປະໂຫຍກລະຫັດຜ່ານຂອງທ່ານ. +\nຮັກສາກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພຫຼາຍ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ (ຫຼືບ່ອນປອດໄພ) + ກະແຈຂອງທ່ານກຳລັງຖືກສຳຮອງໄວ້. + ຄວາມສໍາເລັດ ! + (ຂັ້ນສູງ) ຕັ້ງຄ່າດ້ວຍລະຫັດການກູ້ຂໍ້ມູນ + ຫຼື, ຮັບປະກັນການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍກະແຈການກູ້ຂໍ້ມູນ, ບັນທຶກມັນໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ. + ການສ້າງສໍາຮອງຂໍ້ມູນ + ກຳນົດລະຫັດຜ່ານ + ພວກເຮົາຈະເກັບສຳເນົາລະຫັດທີ່ເຂົ້າລະຫັດໄວ້ຢູ່ໃນເຊີບເວີຂອງທ່ານ. ປົກປ້ອງການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍປະໂຫຍກລະຫັດຜ່ານເພື່ອຮັກສາມັນໃຫ້ປອດໄພ. +\n +\nເພື່ອຄວາມປອດໄພສູງສຸດ, ອັນນີ້ຄວນຈະແຕກຕ່າງຈາກລະຫັດຜ່ານບັນຊີຂອງທ່ານ. + ຮັບປະກັນການສຳຮອງຂໍ້ມູນຂອງທ່ານດ້ວຍລະຫັດຜ່ານ. + ສົ່ງອອກກະແຈດ້ວຍຕົນເອງ + ຢືນຢັນໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ໃນລະບົບອື່ນຂອງທ່ານ: + ຢືນຢັນ + ip ທີ່ບໍ່ຮູ້ຈັກ + ຢັ້ງຢືນແລ້ວ + ບໍ່ມີການຍືນຍັນ + + %1$d/%2$d ນຳເຂົ້າກະແຈສຳເລັດແລ້ວ. + + ຢ່າສົ່ງຂໍ້ຄວາມເຂົ້າລະຫັດໄປຫາລະບົບທີ່ບໍ່ໄດ້ຢືນຢັນຈາກລະບົບນີ້. + ເຂົ້າລະຫັດໄປຫາລະບົບທີ່ຢືນຢັນເທົ່ານັ້ນ + ນໍາເຂົ້າ + ນໍາເຂົ້າລະຫັດຈາກໄຟລ໌ໃນບ່ອນຈັດເກັບ + ນຳເຂົ້າກະແຈຫ້ອງ + ນຳເຂົ້າກະແຈຫ້ອງ E2E + ຈັດການການສໍາຮອງກະແຈ + ການກູ້ຄືນຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ສົ່ງອອກກະແຈສຳເລັດແລ້ວ + ກະລຸນາສ້າງລະຫັດຜ່ານເພື່ອເຂົ້າລະຫັດທີ່ສົ່ງອອກ. ທ່ານຈະຕ້ອງໃສ່ລະຫັດຜ່ານດຽວກັນເພື່ອໃຫ້ສາມາດນໍາເຂົ້າລະຫັດໄດ້. + ສົ່ງອອກ + ສົ່ງອອກກະແຈໄປຍັງບ່ອນເກັບໄຟລ໌ + ສົ່ງອອກກະແຈຫ້ອງ + ສົ່ງອອກກະແຈຫ້ອງ E2E + ລະຫັດລະບົບ + ID ລະບົບ + ຊື່ສາທາລະນະ + ການຖອດລະຫັດຜິດພາດ + ຫົວຂໍ້ + ບໍ່ໄດ້ກຳນົດທີ່ຢູ່ຫຼັກ + ກຳນົດທີ່ຢູ່ຫຼັກ + ລັກສະນະການທົດລອງເຫຼົ່ານີ້ແມ່ນອາດໃຊ້ງານບໍ່ໄດ້. ໃຊ້ດ້ວຍຄວາມລະມັດລະວັງ. + ຫ້ອງທົດລອງ + ເວີຊັ້ນຫ້ອງ + ID ພາຍໃນຫ້ອງນີ້ + ຂັ້ນສູງ + + %d ຜູ້ໃຊ້ທີ່ຖືກຫ້າມ + + ຫ້າມຜູ້ໃຊ້ + ພື້ນທີ່ ຫຼືຫ້ອງອື່ນໆທີ່ທ່ານອາດບໍ່ຮູ້ + ພື້ນທີ່ທີ່ທູກຄົນຮູ້ວ່າມີຫ້ອງນີ້ + ຕັດສິນໃຈວ່າໃຜສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມຫ້ອງນີ້ໄດ້. + ແຕະເພື່ອແກ້ໄຂພຶ້ນທີ່ + ເລືອກພຶ້ນທີ່ + ຕັດສິນໃຈວ່າພື້ນທີ່ໃດສາມາດເຂົ້າເຖິງຫ້ອງນີ້ໄດ້. ຖ້າພື້ນທີ່ຖືກເລືອກ ສະມາຊິກຈະສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມຊື່ຫ້ອງໄດ້. + ພື້ນທີ່ທີ່ສາມາດເຂົ້າເຖິງໄດ້ + ອະນຸຍາດໃຫ້ສະມາຊິກໃນພຶ້ນທີຊອກຫາ ແລະເຂົ້າເຖິງ. + ສະມາຊິກຂອງພຶ້ນທີ່ %s ສາມາດຊອກຫາ, ເບິ່ງຕົວຢ່າງ ແລະເຂົ້າຮ່ວມໄດ້. + ທຸກຄົນຢູ່ໃນພື້ນທີ່ທີ່ມີຫ້ອງນີ້ສາມາດຊອກຫາ ແລະເຂົ້າຮ່ວມໄດ້. ມີແຕ່ຜູ້ເບິ່ງແຍງຫ້ອງນີ້ທີ່ສາມາດເພີ່ມໃສ່ພື້ນທີ່ໄດ້. + ສະມາຊິກຂອງພື້ນທີ່ເທົ່ານັ້ນ + ທຸກຄົນສາມາດຊອກຫາພື້ນທີ່ ແລະ ເຂົ້າຮ່ວມໄດ້ + ທຸກຄົນສາມາດຊອກຫາຫ້ອງ ແລະ ເຂົ້າຮ່ວມໄດ້ + ສາທາລະນະ + ສະເພາະຄົນທີ່ຖືກເຊີນສາມາດຊອກຫາ ແລະ ເຂົ້າຮ່ວມໄດ້ + ສ່ວນຕົວ (ເຊີນເທົ່ານັ້ນ) + ສ່ວນຕົວ + ການຕັ້ງຄ່າການເຂົ້າເຖິງທີ່ບໍ່ຮູ້ຈັກ (%s) + ທຸກຄົນສາມາດເຄາະຫ້ອງໃດກໍ່ໄດ້, ສະມາຊິກສາມາດຍອມຮັບ ຫຼື ປະຕິເສດກໍ່ໄດ້ + ສະມາຊິກເທົ່ານັ້ນ (ນັບຕັ້ງແຕ່ພວກເຂົາເຂົ້າຮ່ວມ) + ສະມາຊິກເທົ່ານັ້ນ (ນັບຕັ້ງແຕ່ພວກເຂົາຖືກເຊີນ) + ສະມາຊິກເທົ່ານັ້ນ (ຕັ້ງແຕ່ຈຸດທີ່ເລືອກຂອງທາງເລືອກນີ້) + ຜູ້ໃດຜູ້ຫນຶ່ງ + ບໍ່ສາມາດດຶງຂໍ້ມູນການເບິ່ງເຫັນລາຍການຫ້ອງປະຈຸບັນໄດ້ (%1$s). + ເຜີຍແຜ່ຫ້ອງນີ້ຕໍ່ສາທາລະນະຢູ່ໃນລາຍການຫ້ອງຂອງ %1$s ບໍ\? + ຍົກເລີກການເຜີຍແຜ່ທີ່ຢູ່ນີ້ + ເຜີຍແຜ່ທີ່ຢູ່ນີ້ + ເພີ່ມທີ່ຢູ່ຂອງບ່ອນຈັດເກັບ + ຫ້ອງນີ້ບໍ່ມີທີ່ຢູ່ບ່ອນຈັດເກັບ + ກໍານົດທີ່ຢູ່ສໍາລັບຫ້ອງນີ້ເພື່ອໃຫ້ຜູ້ໃຊ້ສາມາດຊອກຫາຫ້ອງນີ້ຜ່ານ homeserver ຂອງທ່ານ (%1$s) + ທີ່ຢູ່ບ່ອນຈັດເກັບ + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ໃໝ່ (ເຊັ່ນ: #alias: server) + ຍັງບໍ່ມີທີ່ຢູ່ທີ່ເຜີຍແຜ່ອື່ນເທື່ອ. + ບໍ່ມີທີ່ຢູ່ທີ່ພິມເຜີຍແຜ່ອື່ນໆເທື່ອ, ເພີ່ມທີ່ຢູ່ຂ້າງລຸ່ມນີ້. + ລຶບທີ່ຢູ່ \"%1$s\" ບໍ\? + ຍົກເລີກການເຜີຍແຜ່ທີ່ຢູ່ \"%1$s\" ບໍ\? + ເຜີຍແຜ່ + ເຜີຍແຜ່ທີ່ຢູ່ໃໝ່ດ້ວຍຕົນເອງ + ທີ່ຢູ່ອື່ນໆທີ່ເຜີຍແຜ່: + ນີ້ແມ່ນທີ່ຢູ່ຫຼັກ + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ສາມາດນໍາໃຊ້ໂດຍຜູ້ໃດຜູ້ຫນຶ່ງໃນເຊີບເວີການເຂົ້າຮ່ວມຫ້ອງຂອງທ່ານ. ເພື່ອເຜີຍແຜ່ທີ່ຢູ່, ຈໍາເປັນຕ້ອງຕັ້ງເປັນທີ່ຢູ່ຊ່ອງເກັບກ່ອນ. + ທີ່ຢູ່ທີ່ເຜີຍແຜ່ + ເບິ່ງ ແລະ ຈັດການທີ່ຢູ່ຂອງພື້ນທີ່ນີ້. + ທີ່ຢູ່ຂອງພື້ນທີ່ + ເບິ່ງ ແລະ ຈັດການທີ່ຢູ່ຂອງຫ້ອງນີ້, ແລະການເບິ່ງເຫັນຢູ່ໃນລາຍຊື່ຫ້ອງ. + ທີ່ຢູ່ຫ້ອງ + ອະນຸຍາດໃຫ້ແຂກເຂົ້າຮ່ວມ + ການເຂົ້າເຖິງພື້ນທີ່ + ການເຂົ້າເຖິງຫ້ອງ + ໃຜສາມາດເຂົ້າເຖິງໄດ້ແນ່\? + ການປ່ຽນແປງຜູ້ທີ່ອ່ານປະຫວັດໄດ້ຈະມີຜົນກັບຂໍ້ຄວາມໃນອະນາຄົດຢູ່ໃນຫ້ອງນີ້ເທົ່ານັ້ນ. ການເບິ່ງເຫັນປະຫວັດທີ່ມີຢູ່ແລ້ວຈະບໍ່ປ່ຽນແປງ. + ຜູ້ທີ່ສາມາດອ່ານປະຫວັດໄດ້\? + ການອ່ານປະຫວັດຫ້ອງ + ຕັ້ງຄ່າບັນຊີ + ທ່ານສາມາດຈັດການການແຈ້ງເຕືອນໃນ %1$s. + ກະລຸນາສັງເກດວ່າການກ່າວເຖິງ & ການແຈ້ງເຕືອນຄໍາສໍາຄັນບໍ່ສາມາດໃຊ້ໄດ້ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໃນໂທລະສັບມືຖື. + ຈ້ງໃຫ້ຂ້າພະເຈົ້າຊາບເພື່ອ + ຫົວຂໍ້ + ຖ້າເປີດໃຊ້ງານແລ້ວ, ທ່ານຈະສະເເດງສະຖານະແບບອອບໄລນ໌ຕໍ່ກັບຜູ້ໃຊ້ອື່ນສະເໝີ, ເຖິງແມ່ນວ່າຈະໃຊ້ແອັບພລິເຄຊັນກໍຕາມ. + ໂໝດອອບໄລນ໌ + ປະກົດຕົວ + ຕະຫຼອດໄປ + 1 ເດືອນ + 1 ອາທິດ + 3 ມື້ + ໄຫວພິບ + ຫຼິ້ນສຽງ shutter + ເລືອກ + ແຫຼ່ງສື່ + ເລືອກ + ການບີບອັດ + ສື່ມວນຊົນ + ເລືອກປະເທດ + ຈັດການອີເມວ ແລະເບີໂທລະສັບທີ່ເຊື່ອມຕໍ່ກັບບັນຊີ Matrix ຂອງທ່ານ + ອີເມວ ແລະເບີໂທລະສັບ + ສະແດງຂໍ້ຄວາມທັງໝົດຈາກ %s ບໍ\? +\n +\nກະລຸນາຮັບຊາບວ່າຄຳສັ່ງນີ້ຈະປິດເປີດແອັບຄືນໃໝ່ ແລະ ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ລະຫັດຜ່ານຂອງທ່ານໄດ້ຮັບການປັບປຸງ + ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ + ອັບເດດລະຫັດຜ່ານບໍ່ສຳເລັດ + ລະຫັດຜ່ານໃໝ່ + ລະຫັດປັດຈຸບັນ + ປ່ຽນລະຫັດຜ່ານ + ລະຫັດຜ່ານ + ເບີໂທລະສັບນີ້ຖືກໃຊ້ແລ້ວ. + ທີ່ຢູ່ອີເມວນີ້ຖືກໃຊ້ແລ້ວ. + ກະລຸນາກວດເບິ່ງອີເມວຂອງທ່ານແລະກົດໃສ່ການເຊື່ອມຕໍ່. ເມື່ອສິ່ງນີ້ສຳເລັດແລ້ວ, ກົດສືບຕໍ່. + ເລືອກພາສາ + ພາສາ + ການໂຕ້ຕອບຜູ້ໃຊ້ + ເປີດໃຊ້ \'ການອະນຸຍາດການເຊື່ອມໂຍງ\' ໃນການຕັ້ງຄ່າເພື່ອເຮັດສິ່ງນີ້. + ການເຊື່ອມໂຍງຖືກປິດໃຊ້ງານ + ເຊີບເວີນີ້ບໍ່ມີນະໂຍບາຍໃດໆ. + ຫ້ອງສະຫມຸດພາກສ່ວນທີສາມ + ນະໂຍບາຍເຊີບເວີຂອງທ່ານ + ນະໂຍບາຍ homeserver ຂອງທ່ານ + ນະໂຍບາຍ ${app_name} + ຜູ້ຈັດການການເຊື່ອມໂຍງ + ອະນຸຍາດໃຫ້ການເຊື່ອມໂຍງ + ຂໍ້ມູມເຊີບເວີ + Homeserver + ເຂົ້າສູ່ລະບົບເປັນ + ການຢືນຢັນ + %1$s @ %2$s + ເຫັນຄັ້ງສຸດທ້າຍ + ອັບເດດຊື່ສາທາລະນະ + ຊື່ສາທາລະນະ + ID + ທ່ານສາມາດປິດສິ່ງນີ້ໄດ້ທຸກເວລາໃນການຕັ້ງຄ່າ + ກົດໃສ່ໃບຕອບຮັບເພື່ອອ່ານລາຍລະອຽດ. + ສະແດງໃບຕອບຮັບການອ່ານ + ສະແດງເວລາໃນຮູບແບບ 12 ຊົ່ວໂມງ + ສະແດງເວລາຂອງຂໍ້ຄວາມທັງໝົດ + ຈັດຮູບແບບຂໍ້ຄວາມໂດຍໃຊ້ syntax markdown ກ່ອນທີ່ພວກມັນຈະຖືກສົ່ງໄປ. ອັນນີ້ອະນຸຍາດໃຫ້ຈັດຮູບແບບພິເສດເຊັ່ນ: ການໃຊ້ເຄື່ອງໝາຍດາວເພື່ອສະແດງຂໍ້ຄວາມຕົວອຽງ. + ການຈັດຮູບແບບ Markdown + ໃຫ້ຜູ້ໃຊ້ອື່ນຮູ້ວ່າທ່ານກຳລັງພິມຢູ່. + ສົ່ງການແຈ້ງເຕືອນໃນການພິມ + ເບິ່ງຕົວຢ່າງລິ້ງພາຍໃນການສົນທະນາເມື່ອ homeserver ຂອງທ່ານຮອງຮັບຄຸນສົມບັດນີ້. + ຕົວຢ່າງ URL ແບບInline + ປັກໝຸດຫ້ອງດ້ວຍຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ປັກໝຸດຫ້ອງດ້ວຍການແຈ້ງເຕືອນທີ່ພາດ + ຈໍສະແດງຜົນຫນ້າທໍາອິດ + ປື້ມໂທລະສັບ + ການອະນຸຍາດຕິດຕໍ່ + ຕິດຕໍ່ພື້ນທີ່ຈັດເກັບ + ເປົ້າໝາຍການແຈ້ງເຕືອນ + ການຈັດການເຂົ້າລະຫັດລັບ + ການເຂົ້າລະຫັດລັບ + ໃຊ້ຕົວຈັດການການເຊື່ອມໂຍງເພື່ອຈັດການ bots, bridges, widget ແລະຊຸດສະຕິກເກີ. +\nຜູ້ຈັດການການເຊື່ອມໂຍງຂໍ້ມູນການຕັ້ງຄ່າ, ແລະສາມາດດັດແປງ widget, ສົ່ງການເຊື້ອເຊີນຫ້ອງແລະກໍານົດລະດັບພະລັງງານໃນນາມຂອງທ່ານ. + ການເຊື່ອມໂຢງ + ແອັບພລິເຄຊັນກຳລັງໄດ້ຮັບ PUSH + ຄໍາຮ້ອງສະຫມັກແມ່ນລໍຖ້າສໍາລັບການຊຸກຍູ້ + ທົດສອບ Push + ການລົງທະບຽນ FCM token ກັບ homeserver ບໍ່ສຳເລັດ: +\n%1$s + FCM token ສຳເລັດການລົງທະບຽນກັບ homeserver. + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ ${app_name}. ມັນສາມາດເກີດຂຶ້ນຍ້ອນເຫດຜົນຫຼາຍຢ່າງ. ບາງທີມັນອາດຈະໃຊ້ໄດ້ຖ້າຫາກທ່ານລອງໃຫມ່ໃນພາຍຫຼັງ, ທ່ານຍັງສາມາດກວດເບິ່ງວ່າບໍລິການ Google Play ບໍ່ໄດ້ຖືກຈໍາກັດໃນການນໍາໃຊ້ຂໍ້ມູນໃນການຕັ້ງຄ່າລະບົບ, ຫຼື ວ່າໂມງອຸປະກອນຂອງທ່ານແມ່ນຖືກຕ້ອງ, ຫຼືສາມາດເກີດຂື້ນໃນ ROM ແບບກໍານົດເອງ. + [%1$s] +\nຂໍ້ຜິດພາດນີ້ບໍ່ສາມາດຄວບຄຸມໄດ້ ${app_name} ແລະອີງຕາມ Google, ຂໍ້ຜິດພາດນີ້ຊີ້ໃຫ້ເຫັນວ່າອຸປະກອນມີຫຼາຍເເອັບທີ່ລົງທະບຽນກັບ FCM ຫຼາຍເກີນໄປ. ຂໍ້ຜິດພາດເກີດຂື້ນພຽງແຕ່ໃນກໍລະນີທີ່ມີແອັບຈພນວນຫຼາຍ, ດັ່ງນັ້ນຈຶ່ງບໍ່ສົ່ງຜົນກະທົບຕໍ່ຜູ້ໃຊ້ໂດຍສະເລ່ຍ. + ໃບຢັ້ງຢືນໄດ້ປ່ຽນຈາກອັນທີ່ເຊື່ອຖືກ່ອນໜ້ານີ້ ມາເປັນອັນທີ່ບໍ່ໜ້າເຊື່ອຖືໄດ້. ເຊີບເວີອາດຈະໄດ້ຕໍ່ອາຍຸໃບຢັ້ງຢືນແລ້ວ. ຕິດຕໍ່ຜູ້ເບິ່ງແຍງເໍີບເວີສໍາລັບລາຍນິ້ວທີ່ຕ້ອງການ. + ລາຍການຫ້ອງ + ຄຳເຕືອນ + ຮັບການຊຸກຍູ້ບໍ່ສຳເລັດ. ການແກ້ໄຂອາດຈະເປັນການຕິດຕັ້ງແອັບພລິເຄຊັນໃຫມ່. + ຂັ້ນສູງ + ອື່ນໆ + ຜູ້ໃຊ້ບໍ່ສົນໃຈ + ການແຈ້ງເຕືອນ + ການຕັ້ງຄ່າຜູ້ໃຊ້ + ລ້າງcacheມີເດຍ + ລ້າງ cache + ຮັກສາສື່ + ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ + ລິຂະສິດ + ແຈ້ງການພາກສ່ວນທີສາມ + ຂໍ້ຕົກລົງ & ເງື່ອນໄຂ + ເວີຊັ້ນ olm + ເວີຊັ້ນ + + %d ວິນາທີ + + ມັນເປັນ spam + ບໍ່ມີໄຟລ໌ຢູ່ໃນຫ້ອງນີ້ + %1$s ທີ່ %2$s + ໄຟລ໌ + ບໍ່ມີສື່ຢູ່ໃນຫ້ອງນີ້ + ສື່ມວນຊົນ + %1$d ຂອງ%2$d + ບໍ່ສາມາດຈັດການຂໍ້ມູນການແບ່ງປັນໄດ້ + ໝູນ ແລະຕັດ + ສະຖານທີ່ + ການສໍາຫຼວດ + ສະຕິກເກີ + ຄັງຮູບ + ກ້ອງຖ່າຍຮູບ + ຕິດຕໍ່ + ໄຟລ໌ + ເພີ່ມຮູບພາບຈາກ + ເກີດຄວາມຜິດພາດຂຶ້ນໃນຂະນະທີ່ກຳລັງດຶງໄຟລ໌ແນບມາ. + ໄຟລ໌ໃຫຍ່ເກີນທີ່ຈະອັບໂຫລດ. + + %d ຜູ້ໃຊ້ອ່ານແລ້ວ + + %s ອ່ານແລ້ວ + %1$s ແລະ %2$s ອ່ານແລ້ວ + %1$s, %2$s ແລະ %3$s ອ່ານແລ້ວ + + %1$s, %2$s ແລະ %3$d ຜູ້ອື່ນໆອ່ານ + + ໄປທີ່ດ້ານລຸ່ມ + ປິດປ້າຍການກສຳຮອງຂໍ້ມູນກະເເຈ + ສ້າງຫ້ອງໃຫມ່ + ສ້າງການສົນທະນາໃຫມ່ໂດຍກົງ + ປິດເມນູສ້າງຫ້ອງ… + ເປີດເມນູສ້າງຫ້ອງ + ເປີດຕົວນໍາທາງ + ສົ່ງໄຟລ໌ແນບ + ເບິ່ງຄືວ່າເຊີບເວີໃຊ້ເວລາດົນເກີນໄປໃນການຕອບສະໜອງ, ສິ່ງນີ້ອາດເກີດຈາກການສັນຍານການເຊື່ອມຕໍ່ບໍ່ສະຖຽນ ຫຼື ຂໍ້ຜິດພາດກັບເຊີບເວີ. ກະລຸນາລອງອີກຄັ້ງໃນອີກໄລຍະໜຶ່ງ. + ກະລຸນາລອງໃໝ່ອີກຄັ້ງເມື່ອທ່ານຍອມຮັບຂໍ້ກຳນົດ ແລະເງື່ອນໄຂຂອງ homeserver ຂອງທ່ານ. + ບັນທຶກລະອຽດ ຈະຊ່ວຍໃຫ້ນັກພັດທະນາສະຫນອງບັນທຶກເພີ່ມເຕີມເມື່ອທ່ານສົ່ງ RageShake. ເຖິງແມ່ນວ່າໃນເວລາທີ່ເປີດໃຊ້, ແອັບພລິເຄຊັນຈະບໍ່ບັນທຶກເນື້ອຫາຂໍ້ຄວາມ ຫຼື ຂໍ້ມູນສ່ວນຕົວອື່ນໆກໍ່ຕາມ. + ເປີດໃຊ້ບັນທຶກລາຍລະອຽດ. + ເຫັນດີກັບເງື່ອນໄຂການໃຫ້ບໍລິການຂອງ ເຊີບເວີ(%s) ເພື່ອອະນຸຍາດໃຫ້ຕົວທ່ານສາມາດຄົ້ນຫາໄດ້ໂດຍທີ່ຢູ່ອີເມວ ຫຼື ເບີໂທລະສັບ. + ຕອນນີ້ທ່ານກຳລັງແບ່ງປັນທີ່ຢູ່ອີເມວ ຫຼື ເບີໂທລະສັບໃນເຊີບເວີ %1$s. ທ່ານຈະຕ້ອງເຊື່ອມຕໍ່ຄືນໃໝ່ກັບ %2$s ເພື່ອຢຸດການແບ່ງປັນອີເມວ ຫຼື ເບີໂທລະສັບ. + ການຢືນຢັນລະຫັດບໍ່ຖືກຕ້ອງ. + ລະຫັດ + ຂໍ້ຄວາມຖືກສົ່ງໄປຫາ %s ແລ້ວ. ກະລຸນາໃສ່ລະຫັດຢືນຢັນ. + ເຊີບເວີທີ່ທ່ານເລືອກບໍ່ມີເງື່ອນໄຂການບໍລິການໃດໆ. ດຳເນີນການຕໍ່ຫາກທ່ານໄວ້ວາງໃຈເຈົ້າຂອງການບໍລິການ + ເຊີບເວີບໍ່ມີເງື່ອນໄຂໃນການໃຫ້ບໍລິການ + ກະລຸນາໃສ່ url ເຊີບເວີ + ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີໄດ້ + ໃສ່ URL ເຊີບເວີ + ທ່ານຕົກລົງເຫັນດີທີ່ຈະສົ່ງຂໍ້ມູນນີ້ບໍ\? + ເພື່ອຄົ້ນຫາຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວ, ທ່ານຈໍາເປັນຕ້ອງສົ່ງຂໍ້ມູນການຕິດຕໍ່ (ອີເມວ ແລະ ເບີໂທລະສັບ) ໄປຫາເຊີບເວີຂອງທ່ານ. ພວກເຮົາຄັດ ຂໍ້ມູນຂອງທ່ານກ່ອນທີ່ຈະສົ່ງເພື່ອຄວາມເປັນສ່ວນຕົວ. + ສົ່ງອີເມວ ແລະເບີໂທລະສັບໄປຫາ %s + ໃຫ້ການຍິນຍອມ + ຍົກເລີກການຍິນຍອມຂອງຂ້ອຍ + ລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານເປັນສ່ວນຕົວ. ເພື່ອຄົ້ນຫາຜູ້ໃຊ້ໃນລາຍຊື່ຜູ້ຕິດຕໍ່ຂອງທ່ານ, ພວກເຮົາຕ້ອງການໄດ້ຮັບອະນຸຍາດຈາກທ່ານເພື່ອສົ່ງຂໍ້ມູນການຕິດຕໍ່ໄປຫາເຊີບຂອງທ່ານ. + ທ່ານໄດ້ໃຫ້ການຍິນຍອມທີ່ຈະສົ່ງອີເມວຂອງທ່ານ ແລະ ເບີໂທລະສັບໄປຫາເຊີບເວີນີ້ເພື່ອຄົ້ນຫາຜູ້ໃຊ້ອື່ນໆຈາກຜູ້ຕິດຕໍ່ຂອງທ່ານ. + ສົ່ງອີເມວ ແລະ ເບີໂທລະສັບ + ພວກເຮົາໄດ້ສົ່ງອີເມວການຢືນຢັນໃຫ້ທ່ານໄປຫາ %s, ກະລຸນາກວດສອບອີເມວຂອງທ່ານກາອນ ແລະ ກົດໃສ່ການເຊື່ອມຕໍ່ການຢືນຢັນ + ພວກເຮົາໄດ້ສົ່ງອີເມວຢືນຢັນໃຫ້ທ່ານຫາ %s, ກວດສອບອີເມວຂອງທ່ານ ແລະ ກົດໃສ່ການເຊື່ອມຕໍ່ການຢືນຢັນ + ສາມາດຄົ້ນພົບເບີໂທລະສັບໄດ້ + ການຕັດການເຊື່ອມຕໍ່ຈາກເຊີບເວີຂອງທ່ານຈະເຮັດໃຫ້ຜູ້ໃຊ້ອື່ນຄນຫາທ່ານໄດ້ ແລະ ທ່ານຈະບໍ່ສາມາດເຊີນຄົນອື່ນຜ່ານທາງອີເມລ໌ ຫຼື ໂທລະສັບໄດ້. + ຕົວເລືອກການຄົ້ນພົບຈະປາກົດຂຶ້ນເມື່ອທ່ານໄດ້ເພີ່ມເບີໂທລະສັບ. + ຕົວເລືອກການຄົ້ນພົບຈະປາກົດຂຶ້ນເມື່ອທ່ານໄດ້ເພີ່ມອີເມວ. + ສາມາດຄົ້ນພົບທີ່ຢູ່ອີເມວໄດ້ + ໃນປັດຈຸບັນທ່ານບໍ່ໄດ້ໃຊ້ເຊີບເວີ. ເພື່ອຄົ້ນຫາແລະສາມາດຄົ້ນພົບໄດ້ໂດຍຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວ, ກໍານົດຄ່າຂ້າງລຸ່ມນີ້. + ຕອນນີ້ທ່ານກຳລັງໃຊ້ %1$s ເພື່ອຄົ້ນຫາ ແລະສາມາດຄົ້ນພົບໄດ້ໂດຍຜູ້ຕິດຕໍ່ທີ່ມີຢູ່ແລ້ວທີ່ທ່ານຮູ້ຈັກ. + ບໍ່ມີນະໂຍບາຍສະໜອງໃຫ້ໂດຍເຊີບເວີ + ເຊື່ອງນະໂຍບາຍເຊີບເວີ + ສະແດງນະໂຍບາຍເຊີບ + ປ່ຽນເຊີບເວີ + ເປີດການຕັ້ງຄ່າ ການຄົ້ນພົບ + ຕັ້ງຄ່າເຊີບເວີ + ຕັດການເຊື່ອມຕໍ່ເຊີບເວີ + ຂໍ້ມູນເຊີບເວີ + ໃຊ້ Bots, bridges,widgets ແລະຊຸດສະຕິກເກີ + ໃຫ້ຜູ້ອື່ນສາມາດຄົ້ນຫາໄດ້ + ເງື່ອນໄຂການໃຫ້ບໍລິການ + ເບິ່ງປະຫວັດການແກ້ໄຂ + ຄຳແນະນຳ + ຜູ້ໃຊ້ທີ່ຮູ້ຈັກ + ກຳລັງສ້າງຫ້ອງ… + ລະຫັດ QR + ເພີ່ມດ້ວຍລະຫັດ QR + ສຳເນົາລິ້ງໃສ່ຄລິບບອດແລ້ວ + ເພີ່ມແຖບສະເພາະສໍາລັບການແຈ້ງເຕືອນທີ່ຍັງບໍ່ໄດ້ອ່ານໃນຫນ້າຈໍຫຼັກ. + ເປີດໃຊ້ງານການປັດເພື່ອຕອບກັບໃນທາມລາຍ + ຊອກຫາຊື່ + ຄົ້ນຫາດ້ວຍຊື່, ID ຫຼື mail + ຊື່ ຫຼື ID (#example:matrix.org) + ເບິ່ງລາຍການຫ້ອງ + ສົ່ງຂໍ້ຄວາມໃໝ່ໂດຍກົງ + ສ້າງຫ້ອງໃຫມ່ + ບໍ່ສາມາດຊອກຫາສິ່ງທີ່ທ່ານກໍາລັງຊອກຫາ\? + ກັ່ນຕອງການສົນທະນາ… + ທ່ວງເວລາລະຫວ່າງSync + ໝົດເວລາການຮ້ອງຂໍການຊິງຄ໌ + ເລີ່ມຕົ້ນໃນການboot + ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນກ່ຽວກັບຂໍ້ຄວາມທີ່ເຂົ້າມາເມື່ອແອັບຯຢູ່ໃນພື້ນຫຼັງ. + ບໍ່ມີການຊິງຄ໌ພື້ນຫຼັງ + ${app_name} ຈະຊິງຄ໌ໃນພື້ນຫຼັງເປັນໄລຍະໆຕາມເວລາທີ່ຊັດເຈນ (ກຳນົດໄດ້). +\nນີ້ຈະສົ່ງຜົນກະທົບຕໍ່ການໃຊ້ວິທະຍຸ ແລະແບັດເຕີຣີ, ຈະມີການແຈ້ງເຕືອນແບບຖາວອນທີ່ສະແດງວ່າ ${app_name} ກຳລັງຟັງເຫດການ. + ປັບໃຫ້ເຫມາະສໍາລັບເວລາທີ່ແທ້ຈິງ + ${app_name} ຈະຊິ້ງຂໍ້ມູນໃນພື້ນຫຼັງເພື່ອຮັກສາຊັບພະຍາກອນທີ່ຈຳກັດຂອງອຸປະກອນ (ແບັດເຕີຣີ). +\nອີງຕາມສະຖານະຊັບພະຍາກອນອຸປະກອນຂອງທ່ານ, ການຊິງຄ໌ອາດຈະຖືກເລື່ອນໂດຍລະບົບປະຕິບັດການ. + ເຫມາະສໍາລັບເເບັດເຕີລີ + ຮູບແບບການຊິງຄ໌ພື້ນຫຼັງ + ການຊິ້ງຂໍ້ມູນພື້ນຫຼັງ + ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນສໍາລັບການກ່າວເຖິງ & ຄໍາສໍາຄັນຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດຢູ່ໃນມືຖື. + ການຍົກລະດັບຫ້ອງ + ຂໍ້ຄວາມຈາກ bot + ໂທເຊີນ + ການເຊີນດເຂົ້າຫ້ອງ + ຄໍາສໍາຄັນ + \@room + ເຂົ້າລະຫັດຂໍ້ຄວາມກຸ່ມ + ຂໍ້ຄວາມກຸ່ມ + ເຂົ້າລະຫັດຂໍ້ຄວາມໂດຍກົງ + ຂໍ້ຄວາມໂດຍກົງ + ຊື່ຜູ້ໃຊ້ຂອງຂ້ອຍ + ຊື່ສະແດງຂອງຂ້ອຍ + ຂໍ້ຄວາມທີ່ມີ @room + ຂໍ້ຄວາມທີ່ສົ່ງໂດຍ bot + ເມື່ອຂ້ອຍຖືກເຊີນເຂົ້າຫ້ອງ + ເປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບບັນຊີນີ້ + ສຽງແຈ້ງເຕືອນ + ບໍ່ສົນໃຈການເພີ່ມປະສິດທິພາບ + ຖ້າຫາກຜູ້ໃຊ້ປະອຸປະກອນບໍ່ໄດ້ສຽບປັກອຸດປະກອນ ແລະ ຢຸດຢູ່ໃນໄລຍະໜຶ່ງ, ໂດຍທີ່ໜ້າຈໍປິດຢູ່, ອຸປະກອນຈະເຂົ້າສູ່ໂໝດ Doze ແລະ ປ້ອງກັນບໍ່ໃຫ້ແອັບເຂົ້າເຖິງເຄື່ອຂ່າຍ ແລະ ເລື່ອນວຽກ, ການຊິງຄ໌ ແລະ ການເຕືອນມາດຕະຖານຂອງເຂົາເຈົ້າ. + ${app_name} ບໍ່ໄດ້ຮັບຜົນກະທົບຈາກການເພີ່ມປະສິດທິພາບແບັດເຕີລີ. + ການເພີ່ມປະສິດທິພາບແບັດເຕີລີ + ປິດການນຳໃຊ້ຂໍ້ຈຳກັດ + ຂໍ້ຈຳກັດໃນພື້ນຫຼັງຖືກເປີດໃຊ້ສຳລັບ ${app_name}. +\nວຽກທີ່ແອັບພະຍາຍາມເຮັດຈະຖືກຈຳກັດຢ່າງແຮງໃນຂະນະທີ່ມັນຢູ່ໃນພື້ນຫຼັງ, ແລະ ອັນນີ້ອາດຈະສົ່ງຜົນກະທົບຕໍ່ການແຈ້ງເຕືອນ. +\n%1$s + ການຈຳກັດພື້ນຫຼັງຖືກປິດການນຳໃຊ້ສຳລັບ ${app_name}. ການທົດສອບນີ້ຄວນຈະດໍາເນີນການໂດຍໃຊ້ອິນເຕີເນັດມືຖື (ບໍ່ມີ WIFI). +\n%1$s + ກວດເບິ່ງຂໍ້ຈໍາກັດໃນພື້ນຫລັງ + ເປີດໃຊ້ເລີ່ມຕົ້ນ boot + ການບໍລິການຈະບໍ່ເລີ່ມຕົ້ນເມື່ອອຸປະກອນຖືກເລີ່ມຕົ້ນໃຫມ່, ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນຈົນກວ່າ ${app_name} ຈະເປີດຄັ້ງດຽວ. + ການບໍລິການຈະເລີ່ມເມື່ອອຸປະກອນຖືກເລີ່ມຕົ້ນໃຫມ່. + ເລີ່ມຕົ້ນ boot + ການແຈ້ງເຕືອນໄດ້ຖືກກົດແລ້ວ! + ກະລຸນາກົດໃສ່ການແຈ້ງເຕືອນ. ຖ້າທ່ານບໍ່ເຫັນການແຈ້ງເຕືອນ, ກະລຸນາກວດເບິ່ງການຕັ້ງຄ່າລະບົບ. + ການສະແດງການແຈ້ງເຕືອນ + ທ່ານກຳລັງເບິ່ງການແຈ້ງເຕືອນ! ກົດທີ່ຂ້ອຍ! + (ຂັ້ນສູງ) + ເລີ່ມຕົ້ນການນໍາໃຊ້ການສໍາຮອງຂໍ້ມູນກະແຈ + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ແມ່ນປອດໄພດ້ວຍການເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງ. ພຽງແຕ່ທ່ານ ແລະຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈເພື່ອອ່ານຂໍ້ຄວາມເຫຼົ່ານີ້. +\n +\nສຳຮອງຂໍ້ມູນກະແຈຂອງທ່ານຢ່າງປອດໄພເພື່ອຫຼີກເວັ້ນການສູນເສຍພວກມັນ. + ຂໍ້ຄວາມເຂົ້າລະຫັດບໍ່ເຄີຍສູນເສຍ + ກະລຸນາລຶບລະຫັດຜ່ານຖ້າທ່ານຕ້ອງການ ${app_name} ເພື່ອສ້າງລະຫັດການກູ້ຂໍ້ມູນ. + ປະໂຫຍກລະຫັດຜ່ານອ່ອນເກີນໄປ + ກະລຸນາໃສ່ລະຫັດຜ່ານ + ລະຫັດຜ່ານບໍ່ກົງກັນ + ໃສ່ລະຫັດຜ່ານ + ຢືນຢັນລະຫັດຜ່ານ + ສ້າງລະຫັດຜ່ານ + ບໍ່ພົບ Google Play Services APK ທີ່ຖືກຕ້ອງ. ການແຈ້ງເຕືອນອາດຈະບໍ່ໃຊ້ງານຢ່າງຖືກຕ້ອງ. + %d+ + %1$s: %2$s + ຫຼຸດລົງ + ຂະຫຍາຍ + ຂໍອະໄພ, ເກີດຄວາມຜິດພາດຂຶ້ນ + ກະລຸນາ %s ເພື່ອສືບຕໍ່ໃຊ້ບໍລິການນີ້. + ກະລຸນາ %s ເພື່ອເພີ່ມຂີດຈຳກັດນີ້. + homeserver ນີ້ຮອດຂີດຈຳກັດຜູ້ໃຊ້ປະຈຳເດືອນແລ້ວ. + " homeserver ນີ້ຮອດຂີດຈຳກັດຜູ້ໃຊ້ລາຍເດືອນຂອງມັນແລ້ວ ດັ່ງນັ້ນ <b>ຜູ້ໃຊ້ບາງຄົນຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້</b>." + homeserver ນີ້ ມີຊັບພະຍາກເກີນຂີດຈຳກັດໃດໜຶ່ງ. + ເຊີບເວີ homeserver ນີ້ເກີນຂີດຈຳກັດໃນຊັບພະຍາກອນຂອງມັນ ດັ່ງນັ້ນ <b>ຜູ້ໃຊ້ບາງຄົນຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້</b>. + ຕິດຕໍ່ຜູ້ເບິ່ງແຍງການບໍລິການຂອງທ່ານ + ກົດທີ່ນີ້ເພື່ອເບິ່ງຂໍ້ຄວາມເກົ່າ + ສືບຕໍ່ການສົນທະນາອື່ນໃນຫ້ອງນີ້ + ການສົນທະນາສືບຕໍ່ຢູ່ທີ່ນີ້ + ຫ້ອງນີ້ໄດ້ຖືກປ່ຽນແທນ ແລະບໍ່ມີການເຄື່ອນໄຫວອີກຕໍ່ໄປ. + ກະລຸນາໃສ່ລະຫັດຜ່ານຂອງທ່ານ. + ກະລຸນາໃສ່ຊື່ຜູ້ໃຊ້. + ປິດການນຳໃຊ້ບັນຊີ + ກະລຸນາລືມຂໍ້ຄວາມທັງໝົດທີ່ຂ້ອຍໄດ້ສົ່ງໄປເມື່ອບັນຊີຂອງຂ້ອຍຖືກປິດການນຳໃຊ້ (ຄຳເຕືອນ: ອັນນີ້ຈະເຮັດໃຫ້ຜູ້ໃຊ້ໃນອະນາຄົດເຫັນການສົນທະນາທີ່ບໍ່ຄົບຖ້ວນ) + ສິ່ງນີ້ຈະເຮັດໃຫ້ບັນຊີຂອງທ່ານບໍ່ສາມາດນຳໃຊ້ໄດ້ຢ່າງຖາວອນ. ທ່ານຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້, ແລະບໍ່ມີໃຜຈະສາມາດລົງທະບຽນ ID ຜູ້ໃຊ້ດຽວກັນໄດ້. ນີ້ຈະເຮັດໃຫ້ບັນຊີຂອງທ່ານອອກຈາກຫ້ອງທັງຫມົດທີ່ໄດ້ເຂົ້າຮ່ວມ, ແລະມັນຈະເອົາລາຍລະອຽດບັນຊີຂອງທ່ານອອກຈາກເຊີບເວີຂອງທ່ານ. <b>ການດໍາເນີນການນີ້ບໍ່ສາມາດປີ້ນຄືນໄດ້</b>. +\n +\nການປິດໃຊ້ງານບັນຊີຂອງທ່ານ <b>ບໍ່ໄດ້ເປັນຄ່າເລີ່ມຕົ້ນເຮັດໃຫ້ພວກເຮົາລືມຂໍ້ຄວາມທີ່ທ່ານໄດ້ສົ່ງໄປ</b>. ຖ້າທ່ານຕ້ອງການໃຫ້ພວກເຮົາລືມຂໍ້ຄວາມຂອງທ່ານ, ກະລຸນາຫມາຍໃສ່ໃນກ່ອງຂ້າງລຸ່ມນີ້. +\n +\nການເບິ່ງເຫັນຂໍ້ຄວາມໃນ Matrix ແມ່ນຄ້າຍຄືກັນກັບອີເມວ. ການລືມຂໍ້ຄວາມຂອງພວກເຮົາຫມາຍຄວາມວ່າຂໍ້ຄວາມທີ່ທ່ານໄດ້ສົ່ງໄປຈະບໍ່ຖືກແບ່ງປັນກັບຜູ້ໃຊ້ໃຫມ່ຫຼືບໍ່ໄດ້ລົງທະບຽນ, ແຕ່ຜູ້ໃຊ້ທີ່ລົງທະບຽນແລ້ວທີ່ມີການເຂົ້າເຖິງຂໍ້ຄວາມເຫຼົ່ານີ້ຈະຍັງຄົງມີການເຂົ້າເຖິງສໍາເນົາຂອງພວກເຂົາ. + ປິດການນຳໃຊ້ບັນຊີ + ກວດເບິ່ງດຽວນີ້ + ເພື່ອສືບຕໍ່ນຳໃຊ້ %1$s homeserver ທ່ານຕ້ອງທົບທວນຄືນ ແລະ ຕົກລົງເຫັນດີກັບຂໍ້ກຳນົດ ແລະເງື່ອນໄຂ. + ຮູບແທນຕົວ + ເຫດຜົນ: %1$s + ທ່ານໄດ້ຖືກຫ້າມຈາກ %1$s ໂດຍ %2$s + ທ່ານໄດ້ຖືກລຶບອອກຈາກ %1$s ໂດຍ %2$s + ເຊີນ + ຫ້ອງ + ຫນ້າທໍາອິ + ສ້າງ + ຂໍ້ຄວາມເຂົ້າລະຫັດ + ສຽງດັງ + ງຽບ + ປິດ + Markdown ໄດ້ຖືກປິດໃຊ້ງານ. + ເປີດໃຊ້ Markdown ແລ້ວ. + ສະແດງຂໍ້ມູນກ່ຽວກັບຜູ້ໃຊ້ + ເພື່ອແກ້ໄຂການຈັດການແອັບ Matrix + ເປີດ/ປິດ markdown + ປ່ຽນຮູບແທນຕົວຂອງທ່ານຢູ່ໃນຫ້ອງປັດຈຸບັນນີ້ເທົ່ານັ້ນ + ປ່ຽນຮູບແທນຕົວຂອງຫ້ອງປັດຈຸບັນ + ປ່ຽນຊື່ສະແດງຂອງທ່ານໃນຫ້ອງປັດຈຸບັນເທົ່ານັ້ນ + ປ່ຽນຊື່ສະແດງຂອງທ່ານ + ລຶບຜູ້ໃຊ້ທີ່ມີ ID ໃຫ້ອອກຈາກຫ້ອງນີ້ + ກໍານົດຫົວຂໍ້ຫ້ອງ + ອອກຈາກຫ້ອງ + ເຂົ້າຮ່ວມຫ້ອງຕາມທີ່ຢູ່ໄດ້ລະບຸໃຫ້ + ເຊີນຜູ້ໃຊ້ທີ່ມີ id ໃຫ້ກັບຫ້ອງປະຈຸບັນ + ກຳນົດຊື່ຫ້ອງ + Deops ຜູ້ໃຊ້ທີ່ມີ ID + ກໍານົດລະດັບພະລັງງານຂອງຜູ້ໃຊ້ + ຢຸດເຊົາການບໍ່ສົນໃຈຜູ້ໃຊ້, ສະແດງຂໍ້ຄວາມຂອງພວກເຂົາຕໍ່ໄປ + ບໍ່ສົນໃຈຜູ້ໃຊ້, ເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າໂດຍທ່ານ + ຍົກເລີກການຫ້າມຜູ້ໃຊ້ທີ່ມີ ID + ຫ້າມຜູ້ໃຊ້ທີ່ມີ ID ທີ່ກຳນົດໃຫ້ + ສະແດງການປະຕິບັດ + ຄຳສັ່ງ \"%s\" ຖືກຮັບຮູ້ແຕ່ບໍ່ຮອງຮັບໃນກະທູ້. + ຄໍາສັ່ງ \"%s\" ຕ້ອງການຕົວກໍານົດການເພີ່ມເຕີມ, ຫຼື ບາງພາລາມິເຕີບໍ່ຖືກຕ້ອງ. + ຄຳສັ່ງທີ່ບໍ່ຮູ້ຈັກ: %s + ຄໍາສັ່ງຜິດພາດ + ບໍ່ສົນໃຈ + ການຮ້ອງຂໍການແບ່ງປັນທີ່ສໍາຄັນ + ແບ່ງປັນ + ເລີ່ມການຢັ້ງຢືນ + ລະບົບທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນກໍາລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. +\nຊື່ລະບົບ: %1$s +\nເຫັນຄັ້ງສຸດທ້າຍ: %2$s +\nຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບອື່ນ, ບໍ່ສົນໃຈຄໍາຮ້ອງຂໍນີ້. + ລະບົບທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນຂອງທ່ານ \'%s\' ກໍາລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. + ລະບົບໃໝ່ກຳລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. +\nຊື່ລະບົບ: %1$s +\nເຫັນຄັ້ງສຸດທ້າຍ: %2$s +\nຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບອື່ນ, ບໍ່ສົນໃຈຄໍາຮ້ອງຂໍນີ້. + ທ່ານໄດ້ເພີ່ມລະບົບໃໝ່ \'%s\', ເຊິ່ງກຳລັງຮ້ອງຂໍການເຂົ້າລະຫັດ. + ເພື່ອສືບຕໍ່, ທ່ານຈໍາເປັນຕ້ອງຍອມຮັບເງື່ອນໄຂຂອງການບໍລິການນີ້. + ເລີ່ມກ້ອງລະບົບແທນໜ້າຈໍກ້ອງແບບກຳນົດເອງ. + ໃຊ້ກ້ອງຖ່າຍຮູບລູ້ນດັ່ງເດີມ + ບໍ່ມີ widget ທີ່ໃຊ້ງານຢູ່ + ຈັດການການເຊື່ອມໂຍງ + ເພີ່ມແອັບ Matrix + ບໍ່ມີparameter ທີ່ຕ້ອງການ. + ບໍ່ເຫັນຫ້ອງ %s. + ບໍ່ມີ user_id ໃນການຮ້ອງຂໍ. + ບໍ່ມີ room_id ໃນການຮ້ອງຂໍ. + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເຮັດສິ່ງນັ້ນຢູ່ໃນຫ້ອງນີ້. + ທ່ານບໍ່ໄດ້ຢູ່ໃນຫ້ອງນີ້. + ລະດັບພະລັງງານຈະຕ້ອງເປັນຈຳນວນບວກ. + ສົ່ງຄຳຮ້ອງຂໍບໍ່ສຳເລັດ. + ບໍ່ສາມາດສ້າງ widget ໄດ້. + ອ່ານສື່ທີ່ມີການປ້ອງກັນ DRM + ໃຊ້ໄມໂຄຣໂຟນ + ໃຊ້ກ້ອງຖ່າຍຮູບ + ບລັອກທັງໝົດ + ອະນຸຍາດ + widget ນີ້ຕ້ອງການໃຊ້ຊັບພະຍາກອນຕໍ່ໄປນີ້: + ອອກຈາກກອງປະຊຸມປະຈຸບັນ ແລະປ່ຽນໄປຫາກອງປະຊຸມອື່ນບໍ\? + ຂໍອະໄພ, ເກີດຄວາມຜິດພາດຂຶ້ນໃນຂະນະທີ່ພະຍາຍາມເຂົ້າຮ່ວມກອງປະຊຸມ + ຂໍອະໄພ, ການປະຊຸມການໂທດ້ວຍ Jitsi ບໍ່ຮອງຮັບໃນອຸປະກອນເກົ່າ (ອຸປະກອນທີ່ມີ Android OS ຕ່ຳກວ່າ 6.0) + ID ຫ້ອງ + ໄອດີວິດເຈັດ + ຫົວຂໍ້ຂອງທ່ານ + ID ຜູ້ໃຊ້ຂອງທ່ານ + URL ຮູບແທນຕົວຂອງທ່ານ + ຊື່ສະແດງຂອງທ່ານ + ຖອນສິດການເຂົ້າເຖິງສໍາລັບຂ້ອຍ + ເປີດໃນ browser + ໂຫຼດວິດເຈັດຄືນໃໝ່ + ໂຫຼດ widget ບໍ່ສຳເລັດ. +\n%s + ການໃຊ້ອາດຈະແບ່ງປັນຂໍ້ມູນກັບ %s: + ການໃຊ້ອາດຈະຕັ້ງ cookies ແລະແບ່ງປັນຂໍ້ມູນກັບ %s: + widget ນີ້ຖືກເພີ່ມໂດຍ: + ໂຫຼດ Widget + ວິດເຈັດ + ວິດເຈັດທີ່ໃຊ້ຢູ່ + ເບິ່ງ + + %d ວິດເຈັດທີ່ໃຊ້ຢູ່ + + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບວິດເຈັດອອກຈາກຫ້ອງນີ້\? + ໃຫຍ່ + ໃຫຍ່ທີ່ສຸດ + ໃຫຍ່ກວ່າ + ໃຫຍ່ + ປົກກະຕິ + ນ້ອຍ + ນ້ອຍໆ + ຂະໜາດຕົວອັກສອນ + %1$s: %2$s%3$s + %1$s: %2$s + ** ການສົ່ງ ບໍ່ສໍາເລັດ- ກະລຸນາເປີດຫ້ອງ + ຂ້ອຍ + ເຊີນໃໝ່ + ຂໍ້ຄວາມໃໝ່ + ຫ້ອງ + ເຫດການໃໝ່ + %1$s ແລະ %2$s + %1$s ໃນ %2$s ແລະ %3$s + %1$s ໃນ %2$s + + %d ການແຈ້ງເຕືອນ + + + %1$s: %2$d ຂໍ້ຄວາມ + + + %d ການເຊີນ + + + %d ຫ້ອງ + + + %d ຂໍ້ຄວາມແຈ້ງເຕືອນທີ່ຍັງບໍ່ໄດ້ອ່ານ + + ເຊີບເວີນີ້ມີຢູ່ໃນລາຍຊື່ແລ້ວ + ບໍ່ສາມາດຊອກຫາເຊີບເວີນີ້ ຫຼື ລາຍຊື່ຫ້ອງໄດ້ + ໃສ່ຊື່ຂອງເຊີບເວີໃໝ່ທີ່ທ່ານຕ້ອງການສຳຫຼວດ. + ເພີ່ມເຊິບເວີໃຫມ່ + ເຊີບເວີຂອງທ່ານ + ທັງໝົດ %s ຫ້ອງ + ຫ້ອງທັງໝົດຢູ່ໃນເຊີບເວີ %s + ຊື່ເຊີບເວີ + ເລືອກລາຍການຫ້ອງ + ຖ້າລະຫັດບໍ່ກົງກັນ, ຄວາມປອດໄພຂອງການສື່ສານຂອງທ່ານອາດຈະຖືກທໍາລາຍ. + ຂໍ້ຄວາມໃນກຸ່ມສົນທະນາ + ຂໍ້ຄວາມການສົນທະນາຫນຶ່ງຕໍ່ຫນຶ່ງ + ຂໍ້ຄວາມທີ່ມີຊື່ຜູ້ໃຊ້ຂອງຂ້ອຍ + ຂໍ້ຄວາມທີ່ມີຊື່ສະແດງຂອງຂ້ອຍ + ເມື່ອມີການຍົກລະດັບຫ້ອງ + ຂໍ້ຄວາມເຂົ້າລະຫັດໃນການສົນທະນາກຸ່ມ + ຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດໃນການສົນທະນາແບບຫນຶ່ງຕໍ່ຫນຶ່ງ + ເລືອກສີ LED, ການສັ່ນ, ສຽງ… + ຕັ້ງຄ່າການແຈ້ງເຕືອນງຽບ + ຕັ້ງຄ່າການແຈ້ງເຕືອນການໂທ + ຕັ້ງຄ່າການແຈ້ງເຕືອນສຽງລົບກວນ + ປີດໃຊ້ການແຈ້ງເຕືອນສຳລັບລະບົບນີ້ + ຫ້ອງນີ້ກຳລັງຕິດຕັ້ງເວີຊັ້ນຫ້ອງ %s, ເຊິ່ງ homeserver ນີ້ໄດ້ເຮັດເຄື່ອງໝາຍບໍ່ສະຖຽນໄວ້. + ທ່ານຕ້ອງໄດ້ຮັບການອະນຸຍາດເພື່ອຍົກລະດັບຫ້ອງ + ອັບເດດພຶ້ນທີ່ອັດຕະໂນມັດ + ເຊີນຜູ້ໃຊ້ອັດຕະໂນມັດ + ທ່ານຈະຍົກລະດັບຫ້ອງນີ້ຈາກ %1$s ເປັນ %2$s. + ການຍົກລະດັບຫ້ອງແມ່ນເປັນການດຳເນີນການຂັ້ນສູງ ແລະ ປົກກະຕິແລ້ວແມ່ນແນະນຳເມື່ອຫ້ອງບໍ່ສະຖຽນເນື່ອງຈາກມີຂໍ້ບົກພ່ອງ, ຄຸນສົມບັດທີ່ຂາດຫາຍໄປ ຫຼື ຊ່ອງໂຫວ່ດ້ານຄວາມປອດໄພ. +\nປົກກະຕິແລ້ວນີ້ມີຜົນຕໍ່ການການປະມວນຜົນຫ້ອງຢູ່ໃນເຊີບເວີ. + ຍົກລະດັບຫ້ອງສ່ວນຕົວ + ຍົກລະດັບຫ້ອງສາທາລະນະ + ຕ້ອງການຍົກລະດັບ + ຍົກລະດັບ + ກະລຸນາລໍຖ້າ, ມັນອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ. + ເຂົ້າຮ່ວມຫ້ອງແທນ + ບໍ່ມີຊື່ຫ້ອງ + ບາງຫ້ອງອາດຈະຖືກເຊື່ອງໄວ້ເພາະວ່າເປັນສ່ວນຕົວ ແລະ ທ່ານຕ້ອງໄດ້ຮັບການເຊີນ + ບາງຫ້ອງອາດຈະຖືກເຊື່ອງໄວ້ເພາະວ່າເປັນສ່ວນຕົວ ແລະ ທ່ານຕ້ອງການການເຊີນ. +\nທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເພີ່ມຫ້ອງ. + ພື້ນທີ່ນີ້ບໍ່ມີຫ້ອງ + ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານສຳລັບຂໍ້ມູນເພີ່ມເຕີມ + ເບິ່ງຄືວ່າ homeserver ຂອງທ່ານຍັງບໍ່ຮອງຮັບ ພື້ນທີ່ ເທື່ອ + ຮູ້ສຶກວ່າທົດລອງ\? +\nທ່ານສາມາດເພີ່ມພື້ນທີ່ທີ່ມີຢູ່ໄປຍັງອີກພຶ້ນທີ່ໜຶ່ງ. + ຫ້ອງທັງໝົດທີ່ທ່ານຢູ່ຈະຖືກສະແດງຢູ່ໃນໜ້າທຳອິດ. + ສະແດງຫ້ອງທັງໝົດໃນໜ້າທຳອິດ + ຈັດການຫ້ອງ ແລະ ພື້ນທີ່ຕ່າງໆ + ໝາຍວ່າບໍ່ໄດ້ແນະນຳ + ໝາຍຕາມທີ່ແນະນຳ + ແນະນຳ + ຈັດການຫ້ອງ + ຊອກຫາຄົນທີ່ບໍ່ໄດ້ຢູ່ໃນ %s ບໍ\? + %s ເຊີນທ່ານ + ໝາຍເຫດ: ແອັບຈະຖືກເປີດຄືນໃໝ່ + ເປີດໃຊ້ຂໍ້ຄວາມກະທູ້ + ລະບົບຂອງທ່ານຈະສົ່ງບັນທຶກອັດຕະໂນມັດເມື່ອເກີດຄວາມຜິດພາດທີ່ບໍ່ສາມາດຖອດລະຫັດໄດ້ + ລາຍງານຄວາມຜິດພາດໃນການຖອດລະຫັດອັດຕະໂນມັດ. + ທ່ານໄດ້ຮັບເຊີນ + ພື້ນທີ່ເປັນວິທີໃໝ່ໃນການຈັດກຸ່ມຫ້ອງ ແລະ ຄົນ. + ເພີ່ມພື້ນທີ່ໃສ່ບ່ອນໃດກໍໄດ້ທີ່ທ່ານຈັດການ. + ເພີ່ມພື້ນທີ່ທີ່ມີຢູ່ + ເພີ່ມຫ້ອງທີ່ມີຢູ່ + ເພີ່ມຫ້ອງ ແລະພື້ນທີ່ທີ່ມີຢູ່ + ເລືອກສິ່ງທີ່ຕ້ອງອອກໄປ + ອອກຈາກຫ້ອງ ແລະພື້ນທີ່ສະເພາະ… + ຢ່າອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ໃດໆ + ອອກຈາກຫ້ອງ ແລະ ພື້ນທີ່ທັງໝົດ + ທ່ານພຽງຜູ້ທີ່ເບິ່ງແຍງລະບົບພື້ນທີ່ນີ້. ການອອກຈາກລະບົບນັ້ນຫມາຍຄວາມວ່າບໍ່ມີໃຜຄວບຄຸມມັນ. + ທ່ານຈະບໍ່ສາມາດເຂົ້າຮ່ວມຄືນໃໝ່ໄດ້ເວັ້ນເສຍແຕ່ວ່າທ່ານໄດ້ຮັບເຊີນຄືນໃໝ່. + ທ່ານເປັນຄົນດຽວຢູ່ທີ່ນີ້. ຖ້າທ່ານອອກໄປ, ບໍ່ມີໃຜຈະສາມາດເຂົ້າຮ່ວມໃນອະນາຄົດ, ລວມທັງທ່ານ. + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກ %s\? + ອອກໄປ + ເພີ່ມຫ້ອງ + ສຳຫຼວດຫ້ອງ + + %d ຄົນທີ່ທ່ານຮູ້ຈັກໄດ້ເຂົ້າຮ່ວມຢູ່ກ່ອນແລ້ວ + + ການຄົ້ນພົບ (%s) + ສຳເລັດການຕັ້ງຄ່າ + ເຊີນທາງອີເມວ, ຊອກຫາລາຍຊື່ຜູ້ຕິດຕໍ່ ແລະ ອື່ນໆອີກ… + ສຳເລັດການຕັ້ງຄ່າການຄົ້ນຫາ. + ໃນປັດຈຸບັນນີ້ທ່ານບໍ່ໄດ້ໃຊ້ເຊີບເວີ. ເພື່ອເຊີນເພື່ອນຮ່ວມທີມ ແລະ ສາມາດຄົ້ນຫາໄດ້ໂດຍພວກເຂົາ,ກຳນົດຄ່າຂ້າງລຸ່ມນີ້. + ບໍ່ສາມາດເຂົ້າເຖິງນາມແຝງນີ້ໄດ້ໃນເວລານີ້. +\nລອງໃໝ່ໃນພາຍຫຼັງ, ຫຼື ຖາມຜູ້ເບິ່ງແຍງຫ້ອງເພື່ອກວດເບິ່ງວ່າທ່ານມີການເຂົ້າເຖິງຫຼືບໍ່. + ເຂົ້າຮ່ວມເລີຍ + ສິ້ນສຸດການໂທດ້ວຍວິດີໂອ • %1$s + ສິ້ນສຸດການໂທ • %1$s + ການໂທວິດີໂອ + ໂທດ້ວຍສຽງ + ໂທວິດີໂອເຂົ້າມາ + ສາຍໂທເຂົ້າ + ໂທກັບ + ການໂທນີ້ສິ້ນສຸດລົງ + %1$s ປະຕິເສດການໂທນີ້ + ທ່ານໄດ້ປະຕິເສດການໂທນີ້ + ຍົກເລີກການປ່ຽນແປງ + ມີການປ່ຽນແປງທີ່ບໍ່ໄດ້ບັນທຶກໄວ້. ຍົກເລີກການປ່ຽນແປງບໍ\? + ບໍ່ທັນໄດ້ສ້າງຫ້ອງ. ຍົກເລີກການສ້າງຫ້ອງບໍ\? + ການເຊື່ອມຕໍ່ບໍ່ຖືກຕ້ອງ + ບໍ່ໄດ້ສະແກນລະຫັດ QR! + ລະຫັດ QR ບໍ່ຖືກຕ້ອງ (URI ບໍ່ຖືກຕ້ອງ)! + DM ຕົວເອງບໍ່ໄດ້! + ແບ່ງປັນຂໍ້ຄວາມ + ບໍ່ພົບຫ້ອງນີ້. ໃຫ້ແນ່ໃຈວ່າມີຢູ່. + ບໍ່ສາມາດເປີດຫ້ອງທີ່ຖືກຫ້າມໄດ້. + ປ່ຽນລະຫັດ PIN ປັດຈຸບັນຂອງທ່ານ + ປ່ຽນລະຫັດ PIN + ຕ້ອງໃຊ້ລະຫັດ PIN ທຸກໆຄັ້ງທີ່ທ່ານເປີດ ${app_name}. + ຕ້ອງໃຊ້ລະຫັດ PIN ຫຼັງຈາກບໍ່ໄດ້ໃຊ້ ${app_name}ເປັນເວລາ 2 ນາທີ. + ຕ້ອງການລະຫັດ PIN ຫຼັງຈາກ 2 ນາທີ + ສະແດງສະເພາະແຕ່ຈໍານວນຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານໃນການແຈ້ງເຕືອນເທົ່ານັ້ນ. + ສະແດງລາຍລະອຽດເຊັ່ນ: ຊື່ຫ້ອງ ແລະ ເນື້ອໃນຂໍ້ຄວາມ. + ສະແດງເນື້ອຫາໃນການແຈ້ງເຕືອນ + ລະຫັດ PIN ເປັນວິທີດຽວທີ່ຈະປົດລັອກ ${app_name}. + ເປີດໃຊ້ biometrics ສະເພາະອຸປະກອນ ເຊັ່ນ: ລາຍນິ້ວມື ແລະ ການຮັບຮູ້ໃບໜ້າ. + ເປີດໃຊ້ biometrics + ຖ້າທ່ານຕ້ອງການຕັ້ງຄ່າຄືນລະຫັດ PIN ຂອງທ່ານ, ແຕະ ລືມລະຫັດ PIN ເພື່ອອອກຈາກລະບົບ ແລະຕັ້ງຄ່າຄືນໃໝ່. + ເປີດໃຊ້ງານ PIN + ຕັ້ງຄ່າການປ້ອງກັນ + ປົກປ້ອງການເຂົ້າເຖິງໂດຍໃຊ້ ລະຫັດ PIN ແລະ biometrics. + ປົກປ້ອງການເຂົ້າເຖິງ + ເພື່ອຕັ້ງຄ່າລະຫັດ PIN ຂອງທ່ານ, ທ່ານຈະຕ້ອງເຂົ້າສູ່ລະບົບໃໝ່ ແລະສ້າງລະຫັດ PIN ໃໝ່. + ລະຫັດ PIN ໃໝ່ + ຕັ້ງຄ່າ PIN ຄືນໃໝ່ + ລືມ PIN ບໍ\? + ໃສ່ PIN ຂອງທ່ານ + ການກວດສອບ PIN ບໍ່ສຳເລັດ, ກະລຸນາແຕະລະຫັດໃໝ່. + ຢືນຢັນ PIN + ເລືອກ PIN ເພື່ອຄວາມປອດໄພ + ມີຂໍ້ຜິດພາດຫຼາຍເກີນໄປ, ທ່ານໄດ້ອອກຈາກລະບົບແລ້ວ + ຄໍາເຕືອນ! ຄວາມພະຍາຍາມຄັ້ງສຸດທ້າຍກ່ອນອອກຈາກລະບົບ! + + %d ການລົງທະບຽນ + + + ລະຫັດຜິດ, ພະຍາຍາມອີກ %dຄັ້ງ + + ກວດເບິ່ງການຕັ້ງຄ່າຂອງທ່ານເພື່ອເປີດໃຊ້ການແຈ້ງເຕືອນແບບ push + ການແຈ້ງເຕືອນແບບ Pushຖືກປິດໃຊ້ງານ + ຜູ້ໃຊ້ UnBan ບໍ່ສຳເລັດ + ຫ້າມໂດຍ %1$s + ຍົກເລີກການເຊີນໄປຫາ %1$s ບໍ\? + ຍົກເລີກຄຳເຊີນ + ຄົ້ນຫາລາຍຊື່ຕິດຕໍ່ໃນ Matrix + ບັນຊີຕິດຕໍ່ + ປື້ມບັນທຶກການຕິດຕໍ່ຂອງທ່ານຫວ່າງເປົ່າ + ກຳລັງດຶງຂໍ້ມູນລາຍຊື່ຜູ່ຕິດຕໍ່ຂອງທ່ານ… + ບັນທຶກລະຫັດການກູ້ຄືນໃນ + ຮຽນຮູ້ເພີ່ມເຕີມ + ໄດ້ແລ້ວ + ພວກເຮົາຮູ້ສຶກຕື່ນເຕັ້ນທີ່ຈະປະກາດວ່າພວກເຮົາໄດ້ປ່ຽນຊື່! ແອັບຂອງທ່ານໄດ້ອັບເດດແລ້ວ ແລະ ທ່ານເຂົ້າສູ່ລະບົບບັນຊີຂອງທ່ານແລ້ວ. + Riot ດຽວນີ້ເປັນອົງປະກອບແລ້ວ! + ກຳລັງລໍຖ້າປະຫວັດການເຂົ້າລະຫັດ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເນື່ອງຈາກຜູ້ສົ່ງບໍ່ໄດ້ສົ່ງກະແຈໃຫ້ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເພາະວ່າລະບົບຂອງທ່ານບໍ່ໄດ້ຮັບຄວາມໄວ້ວາງໃຈຈາກຜູ້ສົ່ງ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ເພາະວ່າທ່ານໄດ້ຖືກບັລອກໂດຍຜູ້ສົ່ງ + ເນື່ອງຈາກການເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ, ທ່ານອາດຕ້ອງລໍຖ້າໃຫ້ຂໍ້ຄວາມຂອງຜູ້ໃດຜູ້ໜຶ່ງມາຮອດ ເພາະວ່າກະແຈການເຂົ້າລະຫັດບໍ່ຖືກສົ່ງໃຫ້ທ່ານຢ່າງຖືກຕ້ອງ. + ກຳລັງລໍຖ້າຂໍ້ຄວາມນີ້, ອາດຈະໃຊ້ເວລາຄາວໜຶ່ງ + ທ່ານບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມນີ້ໄດ້ + ກຳນົດຮູບແທນຕົວ + ທ່ານໄດ້ປ່ຽນການຕັ້ງຄ່າຫ້ອງສຳເລັດແລ້ວ + ຫົວຂໍ້ + ຊື່ຫ້ອງ + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພຂອງທ່ານອີກຄັ້ງເພື່ອຢືນຢັນ. + ປະໂຫຍກຄວາມປອດໄພ + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພທີ່ທ່ານຮູ້, ໃຊ້ເພື່ອຮັບປະກັນຄວາມລັບຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ກໍານົດປະໂຫຍກຄວາມປອດໄພ + ເກັບຮັກສາກະແຈຄວາມປອດໄພຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ, ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ ຫຼືບ່ອນປອດໄພ. + ບັນທຶກກະແຈຄວາມປອດໄພຂອງທ່ານ + ກະລຸນາໃສ່ປະໂຫຍກລັບທີ່ທ່ານຮູ້, ແລະສ້າງລະຫັດສໍາລັບການສໍາຮອງຂໍ້ມູນ. + ໃຊ້ປະໂຫຍກຄວາມປອດໄພ + ສ້າງກະແຈຄວາມປອດໄພເພື່ອເກັບຮັກສາໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ ຫຼືບ່ອນປອດໄພ. + ໃຊ້ກະແຈຄວາມປອດໄພ + ຕັ້ງຄ່າ + ປ້ອງກັນການສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດໂດຍການສໍາຮອງລະຫັດຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ການສຳຮອງຂໍ້ມູນທີ່ປອດໄພ + ເລີ່ມກ້ອງຖ່າຍຮູບ + ຢຸດກ້ອງຖ່າຍຮູບ + ເປີດສຽງໄມໂຄຣໂຟນ + ປິດສຽງໄມໂຄຣໂຟນ + ເປີດການສົນທະນາ + ບົດບາດ + ກໍານົດພາລະບົດບາດ + ສົ່ງ + ກະລຸນາໃສ່ URL ຂອງເຊີບເວີ + ອີກທາງເລືອກ, ທ່ານສາມາດໃສ່ URL ຂອງເຊີບເວີອື່ນໆ + ໃຊ້ %1$s + homeserver ຂອງທ່ານ (%1$s) ສະເຫນີໃຫ້ນໍາໃຊ້ %2$s ສໍາລັບ ເຊີບເວີຂອງທ່ານ + ຍັງບໍ່ມີການຍິນຍອມຂອງຜູ້ໃຊ້. + ບໍ່ມີການເຊື່ອມໂຍງໃນປະຈຸບັນກັບຕົວລະບຸນີ້. + ສະມາຄົມດັ່ງກ່າວບໍ່ສຳເລັດ. + ເພື່ອຄວາມເປັນສ່ວນຕົວຂອງທ່ານ, ${app_name} ຮອງຮັບແຕ່ການສົ່ງອີເມວ ແລະ ເບີໂທລະສັບຜູ້ໃຊ້ເທົ່ານັ້ນ. + ກ່ອນອື່ນກະລຸນາຍອມຮັບເງື່ອນໄຂຂອງເຊີບເວີໃນການຕັ້ງຄ່າ. + ກະລຸນາກຳນົດຄ່າເຊີບເວີກ່ອນ. + ການດໍາເນີນງານນີ້ບໍ່ສາມາດເຮັດໄດ້. homeserver ແມ່ນລ້າສະໄຫມ. + ເຊີບເວີນີ້ລ້າສະໄໝແລ້ວ. ${app_name} ຮອງຮັບ API V2 ເທົ່ານັ້ນ. + ຕັດການເຊື່ອມຕໍ່ຈາກເຊີບເວີ %s ບໍ\? + ເປີດເງື່ອນໄຂຂອງ %s + ກຳລັງໂຫຼດພາສາທີ່ມີ… + ພາສາອື່ນໆທີ່ມີຢູ່ + ພາສາປັດຈຸບັນ + ແບ່ງປັນລະຫັດນີ້ກັບຜູ້ຄົນເພື່ອໃຫ້ເຂົາເຈົ້າສາມາດສະແກນໄດ້ ເພື່ອເພີ່ມທ່ານ ແລະເລີ່ມການສົນທະນາ. + ລະຫັດຂອງຂ້ອຍ + ຂໍ້ມູນບັນຊີ + ແບ່ງປັນລະຫັດຂອງຂ້ອຍ + ສະແກນ QR code + ພວກເຮົາບໍ່ສາມາດເຊີນຜູ້ໃຊ້ໄດ້. ກະລຸນາກວດເບິ່ງຜູ້ໃຊ້ທີ່ທ່ານຕ້ອງການເຊີນແລ້ວລອງໃໝ່. + + ສົ່ງຄຳເຊີນໄປໃຫ້ %1$s ແລະ ອີກ %2$d ຄົນ + + ບໍ່ແມ່ນລະຫັດ QR matrix ທີ່ຖືກຕ້ອງ + ສົ່ງຄຳເຊີນໄປຫາ %1$s ແລະ %2$s + ສົ່ງຄຳເຊີນໄປໃຫ້ %1$s + 🔐️ ເຂົ້າຮ່ວມກັບຂ້ອຍໃນ ${app_name} + ສະບາຍດີ, ລົມກັບຂ້ອຍຢູ່ ${app_name}: %s + ເຊີນເພື່ອນ + ເຊີນຜູ້ໃຊ້ + ກຳລັງເຊີນຜູ້ໃຊ້… + ເຊີນ + ເພີ່ມຄົນ + ເພີ່ມສະມາຊິກ + ພວກເຮົາບໍ່ສາມາດສ້າງ DM ຂອງທ່ານໄດ້. ກະລຸນາກວດເບິ່ງຜູ້ໃຊ້ທີ່ທ່ານຕ້ອງການເຊີນແລ້ວລອງໃໝ່. + ລິ້ງທີ່ %1$s ກຳລັງພາທ່ານໄປຫາເວັບໄຊອື່ນ: %2$s. +\n +\nທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການສືບຕໍ່\? + ກວດເບິ່ງລິ້ງນີ້ຄືນໃໝ່ + homeserver ບໍ່ຍອມຮັບຊື່ຜູ້ໃຊ້ທີ່ມີພຽງແຕ່ຕົວເລກ. + ກະລຸນາເລືອກລະຫັດຜ່ານ. + ກະລຸນາເລືອກຊື່ຜູ້ໃຊ້. + ການຕັ້ງຄ່າ Cross Signing ບໍ່ສຳເລັດ + ຢືນຢັນຕົວຕົນຂອງທ່ານໂດຍການຢືນຢັນການເຂົ້າສູ່ລະບົບນີ້, ໃຫ້ສິດການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ. + ຢືນຢັນຕົວຕົນຂອງທ່ານໂດຍການກວດສອບການເຂົ້າສູ່ລະບົບນີ້ ໂດຍລະບົບອຶ່ນຂອງທ່ານ, ທີ່ໃຫ້ສິດການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ. + ຢືນຢັນແບບໂຕ້ຕອບໂດຍອີໂມຈິ + ຢືນຢັນການເຂົ້າສູ່ລະບົບ + ຢືນຢັນຂໍ້ຄວາມດ້ວຍຕົນເອງ + ຢືນຢັນການເຂົ້າສູ່ລະບົບໃໝ່ທີ່ເຂົ້າເຖິງບັນຊີຂອງທ່ານ: %1$s + ຢັ້ງຢືນທຸກລະບົບຂອງທ່ານເພື່ອໃຫ້ແນ່ໃຈວ່າບັນຊີ ແລະ ຂໍ້ຄວາມຂອງທ່ານປອດໄພ + ກວດເບິ່ງບ່ອນທີ່ທ່ານເຂົ້າສູ່ລະບົບ + ເຂົ້າລະຫັດໂດຍອຸປະກອນທີ່ບໍ່ໄດ້ຮັບການຢືນຢັນ + ບໍ່ໄດ້ເຂົ້າລະຫັດ + ສົ່ງຫິມະຕົກ ❄️ + ສົ່ງ confetti 🎉 + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດດ້ວຍຫິມະຕົກ + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດດ້ວຍ confetti + + ສະແດງ %d ອຸປະກອນທີ່ທ່ານສາມາດກວດສອບໄດ້ໃນຕອນນີ້ + + ທ່ານຈະປິດເປີດໃໝ່ໂດຍບໍ່ມີປະຫວັດ, ບໍ່ມີຂໍ້ຄວາມ, ອຸປະກອນທີ່ເຊື່ອຖືໄດ້ ຫຼື ຜູ້ໃຊ້ທີ່ເຊື່ອຖືໄດ້ + ຖ້າຫາກວ່າທ່ານຕັ້ງຄ່າທັງຫມົດ + ເຮັດສິ່ງນີ້ກໍ່ຕໍ່ເມືອທ່ານບໍ່ມີອຸປະກອນອື່ນທີ່ທ່ານສາມາດຢືນຢັນອຸປະກອນນີ້ດ້ວຍ. + ຕັ້ງຄ່າຄືນທຸກຢ່າງ + ລືມ ຫຼື ສູນເສຍຕົວເລືອກການກູ້ຂໍ້ມູນທັງໝົດບໍ\?ຕັ້ງຄ່າຄືນທຸກຢ່າງ + ການເຂົ້າເຖິງບ່ອນຈັດເກັບຂໍ້ມູນທີ່ປອດໄພບໍ່ສຳເລັດ + ເລືອກກະແຈການກູ້ຄືນຂອງທ່ານ, ຫຼື ປ້ອນລະຫັດດ້ວຍຕົນເອງໂດຍການພິມ ຫຼື ວາງຈາກ clipboard ຂອງທ່ານ + ໃຊ້ລະຫັດການກູ້ຂໍ້ມູນ + ໃຊ້ %1$s ຂອງທ່ານ ຫຼືໃຊ້ %2$s ຂອງທ່ານເພື່ອສືບຕໍ່. + ຮອງຮັບສະເພາະຫ້ອງທີ່ເຂົ້າລະຫັດໄວ້ + ບັງຄັບໃຫ້ຍົກເລີກຈາກລະບົບກຸ່ມຂາອອກໃນປະຈຸບັນໃນຫ້ອງທີ່ເຂົ້າລະຫັດ + ໃຊ້ ${app_name} ລ່າສຸດໃນອຸປະກອນອື່ນຂອງທ່ານ: + ຫຼື ລູກຄ້າ Matrix ທີ່ສາມາດcross-signing ໄດ້ + ${app_name} iOS +\n${app_name} Android + ${app_name} Web +\n${app_name} Desktop + ໃຊ້ ${app_name} ລ່າສຸດໃນອຸປະກອນອື່ນຂອງທ່ານ, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} ສໍາລັບ Android, ຫຼື ລູກຄ້າ Matrix ອື່ນທີ່ສາມາດ cross-signing ໄດ້ + ຕັ້ງລະຫັດຜ່ານບັນຊີໃໝ່… + ບໍ່ສາມາດບັນທຶກໄຟລ໌ມີເດຍໄດ້ + ການເປີດໃຊ້ງານການຕັ້ງຄ່ານີ້ຈະເພີ່ມ FLAG_SECURE ໃສ່ກິດຈະກຳທັງໝົດ. ການຕັ້ງຄ່າຄືນໃໝ່ແອັບພລິເຄຊັນເພື່ອໃຫ້ການປ່ຽນແປງ. + ປ້ອງກັນພາບໜ້າຈໍຂອງແອັບພລິເຄຊັນ + ລະຫັດສຳຮອງການກູ້ຄືນກະແຈ + ບໍ່ຮູ້ລະຫັດສຳຮອງຂອງທ່ານ, ທ່ານສາມາດ %s ໄດ້. + ໃຊ້ລະຫັດການກູ້ຄືນ ກະເເຈ/ລະຫັດສຳຮອງຂອງທ່ານ + ໃສ່ລະຫັດການສຳຮອງຂໍ້ມູນຂອງທ່ານເພື່ອດຳເນີນການຕໍ່ໄປ. + ການເກັບຮັກສາລະຫັດລັບໃນ SSSS + ກຳລັງສ້າງລະຫັດ SSSS ຈາກລະຫັດການກູ້ຂໍ້ມູນ + ກຳລັງສ້າງລະຫັດ SSSS ຈາກປະໂຫຍດລະຫັດຜ່ານ (%s) + ກຳລັງສ້າງລະຫັດ SSSS ຈາກຂໍ້ຄວາມປະໂຫຍກລະຫັດຜ່ານ + ກຳລັງຂໍລະຫັດເສັ້ນໂຄ້ງ + ກຳລັງກວດສອບລະຫັດສຳຮອງ (%s) + ກຳລັງກວດສອບລະຫັດສຳຮອງ + ກະລຸນາໃສ່ລະຫັດການກູ້ຂໍ້ມູນ + ບໍ່ແມ່ນລະຫັດການກູ້ຂໍ້ມູນທີ່ຖືກຕ້ອງ + ໃຊ້ File + ໃສ່ %s ຂອງທ່ານເພື່ອສືບຕໍ່ + ຢັ້ງຢືນຕົວທ່ານເອງ ແລະ ຄົນອື່ນເພື່ອຮັກສາການສົນທະນາຂອງທ່ານໃຫ້ປອດໄພ + ມີການຍົກລະດັບການເຂົ້າລະຫັດ + ຂໍ້ຄວາມ… + ບັນຊີນີ້ຖືກປິດການໃຊ້ງານແລ້ວ. + ຊື່ຜູ້ໃຊ້ ແລະ/ຫຼືລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ. ລະຫັດຜ່ານທີ່ປ້ອນເຂົ້າເລີ່ມຕົ້ນຫຼືລົງທ້າຍດ້ວຍການຍະຫວ່າງ, ກະລຸນາກວດສອບມັນ. + ສົ່ງຂໍ້ຄວາມເປັນຂໍ້ຄວາມທຳມະດາ, ໂດຍບໍ່ມີການຕີຄວາມໝາຍວ່າເປັນເຄື່ອງໝາຍmarkdown + ແກ້ໄຂບັນຫາ + ການຕັ້ງຄ່າການແຈ້ງເຕືອນ + ການນໍາເຂົ້າລະຫັດບໍ່ສຳເລັດ + ກຳລັງລໍຖ້າ %s… + ເກືອບຈະແລ້ວ! ກຳລັງລໍຖ້າການຢືນຢັນ… + ເກືອບຈະ! ອຸປະກອນອື່ນສະແດງຫມາຍຖືກບໍ\? + "ຫົວຂໍ້: " + ເພີ່ມຫົວຂໍ້ + %s ເພື່ອແຈ້ງໃຫ້ຄົນຮູ້ວ່າຫ້ອງນີ້ແມ່ນຫຍັງ. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງປະຫວັດຂໍ້ຄວາມໂດຍກົງຂອງທ່ານກັບ %s. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງການສົນທະນານີ້. + ນີ້ແມ່ນຈຸດເລີ່ມຕົ້ນຂອງ %s. + ທ່ານໄດ້ເຂົ້າຮ່ວມແລ້ວ. + %s ເຂົ້າຮ່ວມ. + ທ່ານສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ. + %s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ. + ບໍ່ຮອງຮັບການເຂົ້າລະຫັດທີ່ໃຊ້ໃນຫ້ອງນີ້ + ການເຂົ້າລະຫັດຖືກຕັ້ງຄ່າຜິດ + ບໍ່ໄດ້ເປີດໃຊ້ງານການເຂົ້າລະຫັດ + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. ສຶກສາເພີ່ມເຕີມ ແລະ ຢັ້ງຢືນຜູ້ໃຊ້ໃນໂປຣໄຟລ໌ຂອງເຂົາເຈົ້າ. + ເປີດໃຊ້ງານການເຂົ້າລະຫັດແລ້ວ + ຖ້າທ່ານຍົກເລີກຕອນນີ້, ທ່ານອາດຈະສູນເສຍຂໍ້ຄວາມ & ຂໍ້ມູນທີ່ຖືກເຂົ້າລະຫັດ ຖ້າຫາກທ່ານສູນເສຍການເຂົ້າເຖິງການເຂົ້າສູ່ລະບົບຂອງທ່ານ. +\n +\nນອກນັ້ນທ່ານຍັງສາມາດຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ & ຈັດການກະແຈຂອງທ່ານໃນການຕັ້ງຄ່າ. + + %1$dຜູ້ຄົນ + + ການແຈ້ງເຕືອນ + ການຕັ້ງຄ່າ + ການຕັ້ງຄ່າຫ້ອງ + ການດຳເນິນການຂອງຜູ້ເບິ່ງແຍງ + ເພີ່ມເຕີມ + ສຶກສາເພີ່ມເຕີມ + ການກູ້ຄືນການເຂົ້າລະຫັດ + ຄວາມປອດໄພ + ຂໍ້ຄວາມນີ້ຖືກເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ. +\n +\nຂໍ້ຄວາມຂອງທ່ານຖືກລັອກໄວ້ຢ່າງປອດໄພ ແລະ ມີພຽງແຕ່ທ່ານ ແລະ ຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈສະເພາະເພື່ອປົດລັອກພວກມັນ. + ກະລຸນາຕິດຕໍ່ຜູ້ເບິ່ງແຍງເພື່ອກູ້ຄືນການເຂົ້າລະຫັດໃຫ້ເປັນສະຖານະທີ່ຖືກຕ້ອງ. + ການເຂົ້າລະຫັດໄດ້ກຳນົດຄ່າຜິດ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ຖືກເຂົ້າລະຫັດແບບຕົ້ນທາງຮອດປາຍທາງ. +\n +\nຂໍ້ຄວາມຂອງທ່ານຖືກລັອກໄວ້ຢ່າງປອດໄພ ແລະ ມີພຽງແຕ່ທ່ານ ແລະ ຜູ້ຮັບເທົ່ານັ້ນທີ່ມີກະແຈສະເພາະເພື່ອປົດລັອກພວກມັນ. + ຂໍ້ຄວາມນີ້ບໍ່ໄດ້ເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຸຖິງປາຍທາງ. + ຂໍ້ຄວາມຢູ່ໃນຫ້ອງນີ້ບໍ່ໄດ້ຖືກເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງ. + ກຳລັງລໍຖ້າ %s… + ຢືນຢັນແລ້ວ %s + ຢືນຢັນ %s + ກວດສອບໂດຍການປຽບທຽບ emojis + ຢັ້ງຢືນໂດຍການປຽບທຽບ emoji ແທນ + ຖ້າທ່ານບໍ່ໄດ້ຢູ່ໃນຕໍ່ໜ້າ, ໃຫ້ປຽບທຽບ emoji ແທນ + ບໍ່ສາມາດສະແກນໄດ້ + ສະແກນດ້ວຍອຸປະກອນນີ້ + ສະແກນລະຫັດຂອງພວກເຂົາ + ສະແກນລະຫັດດ້ວຍອຸປະກອນອື່ນຂອງທ່ານ ຫຼື ສະຫຼັບ ແລະສະແກນດ້ວຍອຸປະກອນນີ້ + ສະແກນລະຫັດກັບອຸປະກອນຂອງຜູ້ໃຊ້ອື່ນເພື່ອຢືນຢັນກັນຢ່າງປອດໄພ + ຢືນຢັນລະບົບນີ້ + ການຮ້ອງຂໍການຢັ້ງຢືນ + ສົ່ງການຢັ້ງຢືນແລ້ວ + ທ່ານຍອມຮັບ + %s ຍອມຮັບ + ທ່ານໄດ້ຍົກເລີກ + %s ຖືກຍົກເລີກ + ກຳລັງລໍຖ້າ… + ແບ່ງປັນສະຖານທີ່ປັດຈຸບັນຂອງເຂົາເຈົ້າ + ແບ່ງປັນສະຖານທີ່ຂອງເຂົາເຈົ້າ + ສະຫຼຸບການຢັ້ງຢືນ + ຕອບສະໜອງດ້ວຍ: %s + ການສໍາຫຼວດ + ສະຕິກເກີ + ໄຟລ໌ + ສຽງ + ສຽງ + ຮູບພາບ. + ວິດີໂອ. + "ຫນຶ່ງໃນຕໍ່ໄປນີ້ອາດຈະຖືກທໍາລາຍ: +\n +\n - homeserver ຂອງທ່ານ +\n - homeserver ທີ່ຜູ້ໃຊ້ທີ່ທ່ານກໍາລັງກວດສອບແມ່ນໄດ້ເຊື່ອມຕໍ່ກັບ +\n - ຂອງທ່ານ, ຫຼື ການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງຜູ້ໃຊ້ອື່ນໆ +\n - ຂອງທ່ານ, ຫຼື ອຸປະກອນຂອງຜູ້ໃຊ້ອື່ນ" + ບໍ່ປອດໄພ + ບໍ່ກົງກັນ + ກົງກັນ + ເຂົ້າສູ່ລະບົບທີ່ບໍ່ໜ້າເຊື່ອຖື + ໂດເມນອີເມວຂອງທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ລົງທະບຽນຢູ່ໃນເຊີບເວີນີ້ + ກຳລັງສ້າງພື້ນທີ່… + ກຳລັງສ້າງຫ້ອງ… + ບໍ່ອະນຸຍາດໃຫ້ໃຊ້ອັກສອນບາງຕົວ + ກະລຸນາລະບຸທີ່ຢູ່ຫ້ອງ + ທີ່ຢູ່ນີ້ຖືກໃຊ້ແລ້ວ + ທີ່ຢູ່ຂອງພຶ້ນທີ່ + ທ່ານອາດຈະເປີດໃຊ້ງານຖ້າຫ້ອງຈະຖືກໃຊ້ເພື່ອຮ່ວມມືກັບທີມງານພາຍໃນໃນເຊີບເວີຂອງທ່ານເທົ່ານັ້ນ. ອັນນີ້ບໍ່ສາມາດປ່ຽນແປງໄດ້ໃນພາຍຫຼັງ. + ຫ້າມຜູ້ໃດຜູ້ໜຶ່ງທີ່ບໍ່ໄດ້ເປັນສ່ວນຫນຶ່ງຂອງ %s ຈາກການເຂົ້າຮ່ວມຫ້ອງນີ້ + ເຊື່ອງຂັ້ນສູງ + ສະແດງຂັ້ນສູງ + ເມື່ອເປີດໃຊ້ແລ້ວ, ການເຂົ້າລະຫັດບໍ່ສາມາດຖືກປິດໃຊ້ງານໄດ້. + ເປີດໃຊ້ການເຂົ້າລະຫັດ + ເພິ່ມ( ͡° ͜ʖ ͡°) ຂ້າງໜ້າຂໍ້ຄວາມທຳມະດາ + ເພິ່ມ ¯\\_(ツ)_/¯ຂ້າງໜ້າຂໍ້ຄວາມທໍາມະດາ + ສະແດງຂໍ້ມູນທີ່ເປັນປະໂຫຍດເພື່ອຊ່ວຍແກ້ບັນຫາແອັບພລິເຄຊັນ + ສະແດງຂໍ້ມູນການແກ້ໃຂໃນໜ້າຈໍ + ${app_name} ອາດຈະຂັດຂ້ອງເລື້ອຍໆເມື່ອມີຄວາມຜິດພາດທີ່ບໍ່ຄາດຄິດເກີດຂຶ້ນ + ບໍ່ສຳເລັດ + ສະແດງສະເພາະຜົນໄດ້ຮັບທຳອິດ, ພິມຕົວອັກສອນເພີ່ມເຕີມ… + ລະບົບອື່ນໆ + ລະບົບປະຈຸບັນ + ການຕັ້ງຄ່າ + ກວດພົບການສັ່ນ! + ສັ່ນໂທລະສັບຂອງທ່ານເພື່ອທົດສອບເກນການກວດຫາ + ການກວດຫາ + Rageshake + ໂໝດຜູ້ພັດທະນາເປີດໃຊ້ຄຸນສົມບັດທີ່ເຊື່ອງໄວ້ ແລະ ອາດຈະເຮັດໃຫ້ແອັບພລິເຄຊັນມີຄວາມໝັ້ນຄົງໜ້ອຍລົງ. ສໍາລັບນັກພັດທະນາເທົ່ານັ້ນ! + ຮູບແບບນັກພັດທະນາ + ຕັ້ງຄ່າຂັ້ນສູງ + ການຊິງຄ໌ເບື້ອງຕົ້ນ… + ຄຳອະທິບາຍສັ້ນເກີນໄປ + ລິ້ງ matrix.to ຂອງທ່ານບໍ່ຖືກຕ້ອງ + ລະບົບປັດຈຸບັນແມ່ນສໍາລັບຜູ້ໃຊ້ %1$s ແລະ ທ່ານໃຫ້ຂໍ້ມູນປະຈໍາຕົວສໍາລັບຜູ້ໃຊ້ %2$s. ອັນນີ້ບໍ່ຮອງຮັບໂດຍ ${app_name}. +\nກະລຸນາລຶບຂໍ້ມູນກ່ອນ, ຈາກນັ້ນເຂົ້າສູ່ລະບົບອີກຄັ້ງໃນບັນຊີອື່ນ. + ທ່ານຈະສູນເສຍການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ປອດໄພເວັ້ນເສຍແຕ່ວ່າທ່ານເຂົ້າສູ່ລະບົບເພື່ອກູ້ຄືນການເຂົ້າລະຫັດຂອງທ່ານ. + ລຶບລ້າງຂໍ້ມູນທັງໝົດທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ບໍ\? +\nເຂົ້າສູ່ລະບົບອີກຄັ້ງເພື່ອເຂົ້າເຖິງຂໍ້ມູນບັນຊີ ແລະ ຂໍ້ຄວາມຂອງທ່ານ. + ລຶບລ້າງຂໍ້ມູນ + ລຶບລ້າງຂໍ້ມູນທັງໝົດ + ຄຳເຕືອນ: ຂໍ້ມູນສ່ວນຕົວຂອງທ່ານ (ລວມທັງກະແຈການເຂົ້າລະຫັດ) ຍັງຖືກເກັບໄວ້ໃນອຸປະກອນນີ້. +\n +\nລຶບລ້າງຫາກທ່ານໃຊ້ອຸປະກອນນີ້ແລ້ວ, ຫຼືຕ້ອງການເຂົ້າສູ່ລະບົບບັນຊີອື່ນ. + ລຶບຂໍ້ມູນສ່ວນຕົວ + ລະຫັດຜ່ານ + ເຂົ້າສູ່ລະບົບ + ເຂົ້າສູ່ລະບົບເພື່ອກູ້ການເຂົ້າລະຫັດທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ເທົ່ານັ້ນ. ທ່ານຕ້ອງການໃຫ້ພວກເຂົາອ່ານຂໍ້ຄວາມທີ່ປອດໄພທັງໝົດຂອງທ່ານໃນອຸປະກອນໃດກໍໄດ້. + ຜູ້ເບິ່ງແຍງລະບົບ homeserver (%1$s) ຂອງທ່ານໄດ້ນຳທ່ານອອກຈາກລະບົບບັນຊີ %2$s (%3$s). + ເຂົ້າສູ່ລະບົບ + ທ່ານອອກຈາກລະບົບແລ້ວ + ເຂົ້າສູ່ລະບົບອີກຄັ້ງ + ອາດເກີດຈາກເຫດຜົນຕ່າງໆ: +\n +\n• ທ່ານໄດ້ປ່ຽນລະຫັດຜ່ານຂອງທ່ານໃນລະບົບອື່ນ. +\n +\n• ທ່ານໄດ້ລຶບລະບົບນີ້ອອກຈາກລະບົບອື່ນແລ້ວ. +\n +\n• ຜູ້ເບິ່ງແຍງລະບົບເຊີບເວີຂອງທ່ານໄດ້ຍົກເລີກການເຂົ້າເຖິງຂອງທ່ານດ້ວຍເຫດຜົນດ້ານຄວາມປອດໄພ. + ທ່ານອອກຈາກລະບົບແລ້ວ + ເຫັນໂດຍ + ບໍ່ສາມາດຊອກຫາ homeserver ທີ່ຖືກຕ້ອງໄດ້. ກະລຸນາກວດເບິ່ງຕົວລະບຸຂອງທ່ານ + ນີ້ບໍ່ແມ່ນຕົວລະບຸຜູ້ໃຊ້ທີ່ຖືກຕ້ອງ. ຮູບແບບທີ່ຄາດໄວ້: \'@user:homeserver.org\' + ຖ້າທ່ານບໍ່ຮູ້ລະຫັດຜ່ານຂອງທ່ານ, ໃຫ້ກັບຄືນໄປຕັ້ງຄ່າໃໝ່. + Matrix ID + ຖ້າທ່ານຕັ້ງບັນຊີຢູ່ໃນ homeserver, ໃຊ້ Matrix ID ຂອງທ່ານ (ເຊັ່ນ: @user:domain.com) ແລະລະຫັດຜ່ານຂ້າງລຸ່ມນີ້. + ເຂົ້າສູ່ລະບົບດ້ວຍ Matrix ID + ເຂົ້າສູ່ລະບົບດ້ວຍ Matrix ID + + ສົ່ງຄຳຮ້ອງຂໍຫຼາຍເກີນໄປ. ທ່ານສາມາດລອງໃໝ່ໃນອີກ %1$d ວິນາທີ… + + homeserverນີ້ກຳລັງໃຊ້ເວີຊັນເກົ່າ. ຂໍໃຫ້ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານຍົກລະດັບເວີຊັ້ນ. ທ່ານສາມາດສືບຕໍ່ໄດ້, ແຕ່ບາງຄຸນສົມບັດອາດຈະເຮັດວຽກບໍ່ຖືກຕ້ອງ. + homeserver ລ້າສະໄຫມ + ລະຫັດທີ່ໃສ່ນັ້ນບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດສອບ. + ພວກເຮົາຫາກໍສົ່ງອີເມວຫາ %1$s. +\nກະລຸນາກົດໃສ່ການເຊື່ອມຕໍ່ທີ່ມີເພື່ອສືບຕໍ່ການສ້າງບັນຊີ. + ກະລຸນາກວດເບິ່ງອີເມວຂອງທ່ານ + ຍອມຮັບເງື່ອນໄຂເພື່ອສືບຕໍ່ + ກະລຸນາດໍາເນີນການ captcha + ເລືອກ homeserver ແບບກຳນົດເອງ + ເລືອກບໍລິການ Element Matrix + ເລືອກ matrix.org + ທ່ານຍັງບໍ່ໄດ້ສ້າງບັນຊີເທື່ອ. +\n +\nຢຸດຂະບວນການລົງທະບຽນ\? + ຄຳເຕືອນ + ຊື່ຜູ້ໃຊ້ນັ້ນຖືກໃຊ້ແລ້ວ + ຕໍ່ໄປ + ລະຫັດຜ່ານ + ຊື່ຜູ້ໃຊ້ + ຊື່ຜູ້ໃຊ້ ຫຼື ອີເມລ + ລົງທະບຽນສູງສຸດ %1$s + ເບີໂທລະສັບເບິ່ງຄືວ່າບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດເບິ່ງມັນ + ບີໂທລະສັບສາກົນຕ້ອງເລີ່ມຕົ້ນດ້ວຍ \'+\' + ກະລຸນາໃຊ້ຮູບແບບສາກົນ (ເບີໂທລະສັບຕ້ອງຂຶ້ນຕົ້ນດ້ວຍ \'+\') + ຕໍ່ໄປ + ສົ່ງອີກຄັ້ງ + ໃສ່ລະຫັດ + ພວກເຮົາຫາກໍສົ່ງລະຫັດໄປໃຫ້ %1$s. ປ້ອນໃສ່ທາງລຸ່ມເພື່ອຢັ້ງຢືນວ່າມັນແມ່ນທ່ານ. + ຢືນຢັນເບີໂທລະສັບ + ຕໍ່ໄປ + ເບີໂທລະສັບ (ທາງເລືອກ) + ເບີໂທລະສັບ + ກະລຸນາໃຊ້ຮູບແບບສາກົນ. + ກຳນົດເບີໂທລະສັບເພື່ອອະນຸຍາດໃຫ້ຄົນທີ່ທ່ານຮູ້ຈັກຄົ້ນຫາທ່ານ. + ຕັ້ງເບີໂທລະສັບ + ເບິ່ງຄືວ່າທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ + ຕໍ່ໄປ + ອີເມວ (ທາງເລືອກ) + ອີເມວ + ຕັ້ງອີເມວເພື່ອກູ້ຄືນບັນຊີຂອງທ່ານ. ພາຍຫຼັງ, ທ່ານສາມາດເລືອກໃຫ້ຄົນທີ່ທ່ານຮູ້ຈັກຄົ້ນຫາທ່ານທາງອີເມວຂອງທ່ານ. + ຕັ້ງທີ່ຢູ່ອີເມວ + ລະຫັດຜ່ານຂອງທ່ານຍັງບໍ່ທັນມີການປ່ຽນແປງ. +\n +\nຢຸດຂະບວນການປ່ຽນລະຫັດຜ່ານບໍ\? + ຄຳເຕືອນ + ກັບຄືນສູ່ເຂົ້າສູ່ລະບົບ + ທ່ານໄດ້ອອກຈາກລະບົບທັງໝົດ ແລະຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນ push ອີກຕໍ່ໄປ. ເພື່ອເປີດໃຊ້ການແຈ້ງເຕືອນຄືນໃໝ່, ກະລຸນາເຂົ້າສູ່ລະບົບອີກຄັ້ງໃນແຕ່ລະອຸປະກອນ. + ລະຫັດຜ່ານຂອງທ່ານໄດ້ຖືກຕັ້ງຄ່າແລ້ວ. + ຄວາມສໍາເລັດ! + ຂ້ອຍໄດ້ຢືນຢັນທີ່ຢູ່ອີເມວຂອງຂ້ອຍແລ້ວ + ແຕະທີ່ລິ້ງເພື່ອຢືນຢັນລະຫັດຜ່ານໃໝ່ຂອງທ່ານ. ເມື່ອທ່ານໄດ້ປະຕິບັດຕາມການເຊື່ອມຕໍ່ທີ່ມີ, ໃຫ້ກົດສ່ຂ້າງລຸ່ມນີ້. + ອີເມລ໌ຢັ້ງຢືນໄດ້ຖືກສົ່ງໄປຫາ %1$s. + ກວດເບິ່ງກ່ອງຈົດໝາຍຂອງທ່ານ + ອີເມວນີ້ບໍ່ໄດ້ເຊື່ອມຕໍ່ກັບບັນຊີໃດໆ + ສືບຕໍ່ + ການປ່ຽນລະຫັດຜ່ານຂອງທ່ານຈະຕັ້ງຄ່າການເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງໃນທຸກລະບົບຂອງທ່ານ, ເຮັດໃຫ້ປະຫວັດການສົນທະນາທີ່ເຂົ້າລະຫັດໄວ້ບໍ່ສາມາດອ່ານໄດ້. ຕັ້ງຄ່າການສໍາຮອງກະແຈ ຫຼືສົ່ງອອກກະແຈຫ້ອງຂອງທ່ານຈາກລະບົບອື່ນກ່ອນທີ່ຈະຕັ້ງຄ່າລະຫັດຜ່ານຂອງທ່ານ. + ເຕືອນໄພ! + ລະຫັດຜ່ານໃໝ່ + ອີເມວ + ຕໍ່ໄປ + ອີເມວຢືນຢັນຈະຖືກສົ່ງໄປຫາກ່ອງຂໍ້ຄວາມຂອງທ່ານເພື່ອຢືນຢັນການຕັ້ງລະຫັດຜ່ານໃໝ່ຂອງທ່ານ. + ຕັ້ງລະຫັດຜ່ານໃໝ່ໃນ %1$s + ອີເມວນີ້ບໍ່ໄດ້ເຊື່ອມໂຍງກັບບັນຊີໃດໆ. + ທ່ານຖືກຈັບໄດ້ໝົດແລ້ວ! + ແອັບພລິເຄຊັນບໍ່ສາມາດສ້າງບັນຊີຢູ່ໃນ homeserver ນີ້. +\n +\nທ່ານຕ້ອງການລົງທະບຽນໂດຍໃຊ້ web client ບໍ\? + ຂໍອະໄພ, ເຊີບເວີນີ້ບໍ່ຮັບບັນຊີໃໝ່. + ແອັບພລິເຄຊັນບໍ່ສາມາດເຂົ້າສູ່ລະບົບ homeserver ນີ້ໄດ້. homeserver ຮອງຮັບປະເພດການເຂົ້າສູ່ລະບົບຕໍ່ໄປນີ້: %1$s. +\n +\nທ່ານຕ້ອງການເຂົ້າສູ່ລະບົບໂດຍໃຊ້ web client ບໍ\? + ເກີດຄວາມຜິດພາດຂຶ້ນໃນເວລາໂຫລດໜ້າ: %1$s (%2$d) + ໃສ່ທີ່ຢູ່ຂອງເຊີບເວີທີ່ທ່ານຕ້ອງການໃຊ້ + ໃສ່ທີ່ຢູ່ຂອງ Modular Element ຫຼື Server ທີ່ທ່ານຕ້ອງການໃຊ້ + ການເປັນເຈົ້າພາບເປັນທີ່ນິຍົມສໍາລັບອົງການຈັດຕັ້ງ + ທີ່ຢູ່ + ທີ່ຢູ່ບໍລິການ Element Matrix + ລ້າງປະຫວັດ + ສືບຕໍ່ກັບລະບົບປະຕູດຽວ(SSO) + ເຂົ້າສູ່ລະບົບ + ລົງທະບຽນ + ເຂົ້າສູ່ລະບົບ %1$s + ເຊື່ອມຕໍ່ກັບເຊີບເວີທີ່ກໍາຫນົດເອງ + ເຊື່ອມຕໍ່ກັບການບໍລິການ Element Matrix + ເຊື່ອມຕໍ່ຫາ %1$s + ສືບຕໍ່ + ລະບົບປະຕູດຽວ + ເຂົ້າສູ່ລະບົບດ້ວຍ %s + ເຂົ້າຮ່ວມ ພື້ນທີ່ + ສ້າງພື້ນທີ່ + ຂ້າມໄປດຽວນີ້ + ເຂົ້າຮ່ວມພື້ນທີ່ຂອງຂ້ອຍ %1$s%2$s + ເຂົາເຈົ້າຈະບໍ່ເປັນສ່ວນຫນຶ່ງຂອງ %s + ພຽງແຕ່ຫ້ອງນີ້ + ພວກເຂົາຈະສາມາດສຳຫຼວດ %s + ເຊີນເຂົ້າຮ່ວມ %s + ແບ່ງປັນລິ້ງ + ເຊີນໂດຍຊື່ຜູ້ໃຊ້ ຫຼື ທາງໄປສະນີ + ເຊີນຜ່ານອີເມລ໌ + ມີພຽງທ່ານໃນປັດຈຸບັນ. %s ຈະດີກວ່າຖ້າໃຊ້ຮ່ວມກັບຄົນອື່ນ. + ເຊີນ %s + ເຊີນຄົນ + ເຊີນຄົນເຂົ້າມາໃນພື້ນທີ່ຂອງທ່ານ + ລາຍລະອຽດ + ກຳລັງສ້າງພື້ນທີ່… + ສຸ່ມ + ທົ່ວໄປ + ໃຫ້ສ້າງຫ້ອງສໍາລັບພວກເຂົາແຕ່ລະຄົນ. ທ່ານສາມາດເພີ່ມເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ, ລວມທັງລາຍການທີ່ມີຢູ່ແລ້ວ. + ເຈົ້າກຳລັງເຮັດວຽກຫຍັງແດ່\? + ໃຫ້ແນ່ໃຈວ່າຄົນທີ່ມີສິດເຂົ້າເຖິງບໍລິສັດ %s. ທ່ານສາມາດເຊີນເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ. + ໃຜເປັນເພື່ອນຮ່ວມທີມຂອງທ່ານ\? + ພວກເຮົາຈະສ້າງຫ້ອງສໍາລັບພວກເຂົາ. ທ່ານສາມາດເພີ່ມເພີ່ມເຕີມໄດ້ໃນພາຍຫຼັງ. + ການສົນທະນາໃດນຶ່ງທີ່ທ່ານຕ້ອງການມີຢູ່ໃນ %s\? + ຕັ້ງຊື່ເພື່ອສືບຕໍ່. + ເພີ່ມລາຍລະອຽດບາງຢ່າງເພື່ອຊ່ວຍໃຫ້ຜູ້ອຶ່ນລະບຸຕົວຕົນໄດ້. ທ່ານສາມາດປ່ຽນສິ່ງເຫຼົ່ານີ້ໄດ້ທຸກເວລາ. + ເພີ່ມລາຍລະອຽດບາງຢ່າງເພື່ອຊ່ວຍໃຫ້ໂດດເດັ່ນ. ທ່ານສາມາດປ່ຽນສິ່ງເຫຼົ່ານີ້ໄດ້ທຸກເວລາ. + ສ້າງພື້ນທີ່ + ເຊີນເທົ່ານັ້ນ, ທີ່ດີສຸດສຳລັບຕົວທ່ານເອງ ຫຼື ທີມງານ + ສ່ວນຕົວ + ເປີດໃຫ້ທຸກຄົນ, ທີ່ດີສຸດສຳລັບຊຸມຊົນ + ສາທາລະນະ + ພື້ນທີ່ສ່ວນຕົວສຳລັບ ທ່ານ ແລະ ເພື່ອນຮ່ວມທີມ + ຂ້ອຍ ແລະ ເພື່ອນຮ່ວມທີມ + ພື້ນທີ່ສ່ວນຕົວເພື່ອຈັດລະບຽບຫ້ອງຂອງທ່ານ + ພຽງແຕ່ຂ້ອຍ + ໃຫ້ແນ່ໃຈວ່າບຸກຄົນທີ່ມີສິດເຂົ້າເຖິງ %s. + ທ່ານກໍາລັງເຮັດວຽກກັບໃຜ\? + ເພື່ອເຂົ້າຮ່ວມພື້ນທີ່ທີ່ມີຢູ່, ທ່ານຕ້ອງໄດ້ຮັບເຊີນ. + ທ່ານສາມາດປ່ຽນສິ່ງນີ້ໃນພາຍຫຼັງ + ທ່ານຕ້ອງການສ້າງພື້ນທີ່ປະເພດໃດ\? + ພື້ນທີ່ສ່ວນຕົວຂອງທ່ານ + ພື້ນທີ່ສາທາລະນະຂອງທ່ານ + ເພີ່ມພື້ນທີ່ + ພື້ນທີ່ສ່ວນຕົວ + ພື້ນທີ່ສາທາລະນະ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ສົ່ງທັງໝົດຢູ່ໃນຫ້ອງນີ້\? + ລຶບຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ສົ່ງ + ສົ່ງຂໍ້ຄວາມບໍ່ສຳເລັດ + ທ່ານຕ້ອງການຍົກເລີກການສົ່ງຂໍ້ຄວາມບໍ\? + ລຶບຂໍ້ຄວາມທີ່ບໍ່ສຳເລັດທັງໝົດອອກ + ບໍ່ສຳເລັດ + ສົ່ງແລ້ວ + ກຳລັງສົ່ງ + ຍົກລະດັບຫ້ອງເປັນເວີຊັນໃໝ່ + ອອກຈາກຫ້ອງດ້ວຍ id (ຫຼື ຫ້ອງປະຈຸບັນຖ້າເປັນ null) + ເຂົ້າຮ່ວມພື້ນທີ່ກັບ id ທີ່ກຳນົດໃຫ້ + ເພີ່ມໃສ່ພື້ນທີ່ທີ່ກຳນົດໃຫ້ + ສ້າງພື້ນທີ່ + ເນື້ອໃນການນັດໝາຍ + ສົ່ງການນັດໝາຍຂອງລັດ! + ສົ່ງນັດໝາຍ! + ເຫດການຜິດປົກກະຕິ + ບໍ່ມີປະເພດຂໍ້ຄວາມ + ບໍ່ມີເນື້ອຫາ + ເນື້ອໃນການນັດໝາຍ + ສະຖານະຂອງກະເເຈ + ປະເພດ + ສົ່ງນັດໝາຍຂອງລັດແບບກຳນົດເອງ + ແກ້ໄຂເນື້ອຫາ + ການນັດໝາຍຂອງລັດ + ສົ່ງນັດມາຍຂອງລັດ + ສົ່ງນັດໝາຍທີ່ກຳນົດເອງ + ສຳຫຼວດສະຖານະຫ້ອງ + ເຄື່ອງມືພັດທະນາ + ບໍ່ສາມາດໃຊ້ໄດ້ + ອອຟລາຍ + ອອນລາຍ + ຫ້ອງສາທາລະນະ + ເບິ່ງໃບຕອບຮັບການອ່ານ + ບໍ່ໄດ້ແຈ້ງເຕືອນ + ແຈ້ງເຕືອນໂດຍບໍ່ມີສຽງ + ແຈ້ງເຕືອນດ້ວຍສຽງ + ບໍ່ໄດ້ສົ່ງຂໍ້ຄວາມເນື່ອງຈາກຄວາມຜິດພາດ + ບໍ່ໄດ້ເລືອກ + ກວດແລ້ວ + ເປິດຕົວເລືອກ Emoji + ເປີດຕົວເລືອກ Emoji + ລະດັບຄວາມໄວ້ວາງໃຈໄດ້ + ລະດັບຄວາມເຊື່ອໝັ້ນຂອງຄຳເຕືອນ + ລະດັບຄວາມເຊື່ອຖືໃນເບື້ອງເລີ່ມຕົ້ນ + ເລືອກແລ້ວ + ວິດີໂອ + ມີສະບັບຮ່າງທີ່ຍັງບໍ່ໄດ້ສົ່ງ + ບາງຂໍ້ຄວາມຍັງບໍ່ໄດ້ສົ່ງໄປ + ລຶບຮູບແທນຕົວ + ປ່ຽນຮູບແທນຕົວ + ຮູບພາບ + ນໍາເຂົ້າລະຫັດຈາກໄຟລ໌ + ເປີດ widget + ພາບໜ້າຈໍ + ການຢືນຢັນບໍ່ສຳເລັດ + ${app_name} ຮ້ອງຂໍໃຫ້ທ່ານໃສ່ຂໍ້ມູນປະຈໍາຕົວຂອງທ່ານເພື່ອດໍາເນີນການນີ້. + ຕ້ອງການພິສູດການຢືນຢັນຄືນໃໝ່ + ເລື່ອນເພື່ອວາງສາຍ + ບຸກຄົນທີ່ບໍ່ຮູ້ຈັກ + ໂອນໄປ %1$s + ໃຫ້ຄໍາປຶກສາກັບ %1$s + ຜູ້ໃຊ້ + ເກີດຄວາມຜິດພາດໃນລະຫວ່າງການໂອນສາຍ + ໂອນ + ເຊື່ອມຕໍ່ + ປຶກສາກ່ອນ + %1$s ແຕະເພື່ອກັບຄືນ + ການໂທເຂົ້າ (%1$s) · + + %1$d ສາຍໂທເຂົ້າ. + + ການໂທເຂົ້າ (%1$s) + ກີດຄວາມຜິດພາດໃນການຊອກຫາເບີໂທລະສັບ + ປຸ່ມກົດ + ບໍ່ຮັບສາຍ + ສາຍວິດີໂອທີ່ບໍ່ໄດ້ຮັບ + ສາຍບໍ່ໄດ້ຮັບ + ປະຕິເສດການໂທວິດີໂອ + ການໂທດ້ວຍສຽງຖືກປະຕິເສດ + ສະໝັກດ້ວຍ %s + ສືບຕໍ່ດ້ວຍ %s + ຫຼື + ການຕັ້ງຄ່າແບບກຳນົດເອງ ແລະຂັ້ນສູງ + ອື່ນໆ + ສຶກສາເພີ່ມເຕີມ + ການເປັນເຈົ້າພາບເປັນທີ່ນິຍົມສໍາລັບອົງການຈັດຕັ້ງ + ເຂົ້າຮ່ວມຫຼາຍລ້ານຄົນໄດ້ຟຣີໃນເຊີບເວີສາທາລະນະທີ່ໃຫຍ່ທີ່ສຸດ + ເຊັ່ນກຽວກັບອີເມລ໌, ບັນຊີມີອັນດຽວ, ເຖິງແມ່ນວ່າທ່ານສາມາດລົມກັບໃຜກໍຕາມ + ເລືອກເຊີບເວີ + ຂ້ອຍມີບັນຊີຢູ່ແລ້ວ + ສ້າງບັນຊີ + ເລີ່ມຕົ້ນ + ຂະຫຍາຍ ແລະ ປັບແຕ່ງປະສົບການຂອງທ່ານ + ຮັກສາການສົນທະນາສ່ວນຕົວດ້ວຍການເຂົ້າລະຫັດ + ສົນທະນາກັບຄົນໂດຍກົງຫຼືເປັນກຸ່ມ + ເປັນການສົນທະນາຂອງທ່ານ. ເປັນເຈົ້າຂອງ ຂອງມັນ. + ຂ້າມຂັ້ນຕອນນີ້ + ບັນທຶກ ແລະ ສືບຕໍ່ + ບັນທຶກຄວາມມັກຂອງທ່ານໄວ້. + ທ່ານພ້ອມແລ້ວ! + ໄປກັນເລີຍ + ທ່ານສາມາດປ່ຽນອັນນີ້ໄດ້ທຸກເວລາ. + ເພີ່ມຮູບໂປຣໄຟລ໌ + ທ່ານສາມາດປ່ຽນອັນນີ້ໃນພາຍຫຼັງ + ຊື່ສະແດງ + ສຳເນົາໃສ່ບ່ອນຈັດເກັບຂໍ້ມູນຄລາວສ່ວນຕົວຂອງທ່ານ + ບັນທຶກມັນໄວ້ໃນກະແຈ USB ຫຼື ໄດຣຟ໌ສຳຮອງ + ພິມອອກ ແລະ ເກັບຮັກສາໄວ້ບ່ອນທີ່ປອດໄພ + %2$s & %1$s ຂອງທ່ານໄດ້ຕັ້ງຄ່າແລ້ວ. +\n +\nຮັກສາຄວາມປອດໄພ! ທ່ານຈະຕ້ອງປົດລັອກຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ ແລະ ຂໍ້ມູນທີ່ປອດໄພຖ້າຫາກວ່າທ່ານສູນເສຍການເຂົ້າຮ່ວມທັງຫມົດຂອງທ່ານ. + ການຕັ້ງຄ່າສໍາຮອງຂໍ້ມູນກະແຈ + ການຊິ້ງຂໍ້ມູນລະຫັດດ້ວຍຕົນເອງ + ກຳລັງຊິ້ງລະຫັດຜູ້ໃຊ້ + ກຳລັງຊິ້ງລະຫັດຫຼັກ + ກໍານົດລະຫັດເລີ່ມຕົ້ນ SSSS + ກຳລັງສ້າງກະແຈທີ່ປອດໄພຈາກປະໂຫຍກລະຫັດຄວາມປອດໄພ + ການເຜີຍແຜ່ລະຫັດທີ່ສ້າງຂຶ້ນ + ຈົບ + ຮັກສາຄວາມປອດໄພ + ທ່ານສຳເລັດແລ້ວ! + ຕັ້ງຄ່າການກູ້ຂໍ້ມູນ. + ອັນນີ້ອາດຈະໃຊ້ເວລາຫຼາຍວິນາທີ, ກະລຸນາລໍຖ້າ. + ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພທີ່ທ່ານຮູ້, ໃຊ້ເພື່ອຮັບປະກັນຄວາມລັບຢູ່ໃນເຊີບເວີຂອງທ່ານ. + ຢ່າໃຊ້ລະຫັດຜ່ານບັນຊີຂອງທ່ານ. + ໃສ່ %s ຂອງທ່ານເພື່ອສືບຕໍ່. + ກະແຈຂໍ້ຄວາມ + ປະໂຫຍກລະຫັດຜ່ານການກູ້ຄືນ + ຍົກເລີກການຢັ້ງຢືນແລ້ວ + ການຢັ້ງຢືນໄດ້ຖືກຍົກເລີກ. ທ່ານສາມາດເລີ່ມຕົ້ນການຢັ້ງຢືນອີກຄັ້ງ. + ຫນຶ່ງໃນຕໍ່ໄປນີ້ອາດຈະຖືກໂຈມຕີ: +\n +\n- ລະຫັດຜ່ານຂອງທ່ານ +\n- homeserverຂອງທ່ານ +\n- ອຸປະກອນນີ້, ຫຼື ອຸປະກອນອື່ນໆ +\n- ການເຊື່ອມຕໍ່ອິນເຕີເນັດອຸປະກອນການນໍາໃຊ້ +\n +\nພວກເຮົາແນະນຳໃຫ້ທ່ານປ່ຽນລະຫັດຜ່ານ ແລະ ກະແຈການກູ້ຂໍ້ມູນຂອງທ່ານໃນການຕັ້ງຄ່າທັນທີ. + ທ່ານຈະບໍ່ຖືກຢືນຢັນ %1$s (%2$s) ຖ້າຫາກທ່ານຍົກເລີກດຽວນີ້. ເລີ່ມຕົ້ນອີກຄັ້ງໃນໂປຣໄຟລ໌ຜູ້ໃຊ້ຂອງພວກເຂົາ. + ຖ້າຫາກທ່ານຍົກເລີກ, ທ່ານຈະບໍ່ສາມາດອ່ານຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດຢູ່ໃນອຸປະກອນໃຫມ່ຂອງທ່ານໄດ້, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະບໍ່ເຊື່ອມັນ + ຖ້າຫາກທ່ານຍົກເລີກ, ທ່ານຈະບໍ່ສາມາດອ່ານຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ໃນອຸປະກອນນີ້ໄດ້, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະບໍ່ເຊື່ອຖື + ບັນຊີຂອງທ່ານອາດຈະຖືກໂຈມຕີ + ນີ້ບໍ່ແມ່ນຂ້ອຍ + ໃຊ້ລະບົບນີ້ເພື່ອຢືນຢັນໃໝ່ຂອທ່ານ, ໃຫ້ເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້. + ເຂົ້າສູ່ລະບົບໃໝ່. ນີ້ແມ່ນທ່ານບໍ\? + ໂຫຼດຫນ້າຈໍຄືນ + ປົດລັອກປະຫວັດຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ + ກວດສອບການສົ່ງອອກ + ການຮ້ອງຂໍທີ່ສຳຄັນ + ${app_name} Android + ລະຫັດແມ່ນທັນສະໄຫມແລ້ວ! + ເຫດການຖືກກວດສອບໂດຍຜູ້ເບິ່ງແຍງຫ້ອງ, ເຫດຜົນ: %1$s + ເຫດການທີ່ລຶບໂດຍຜູ້ໃຊ້, ເຫດຜົນ: %1$s + ເຫດຜົນສໍາລັບການແກ້ໄຂ + ລວມທັງເຫດຜົນ + ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບ (ລຶບ) ເຫດການນີ້ອອກ\? ກະລຸນາຮັບຊາບວ່າຖ້າຫາກທ່ານລຶບຊື່ຫ້ອງ ຫຼື ການປ່ຽນຫົວຂໍ້, ມັນສາມາດຍົກເລີກການປ່ຽນແປງໄດ້. + ຢືນຢັນການລຶບອອກ + ສົ່ງສື່ຕາມຂະຫນາດຕົ້ນສະບັບ + + ສົ່ງວິດີໂອຕາມຂະຫນາດຕົ້ນສະບັບ + + + ສົ່ງຮູບພາບຕາມຂະຫນາດຕົ້ນສະບັບ + + ທ່ານຕ້ອງການສົ່ງໄຟລ໌ແນບນີ້ໄປຫາ %1$s ບໍ\? + ລຶບອອກ… + ບໍ່ສາມາດຊອກຫາຄວາມລັບຢູ່ໃນບ່ອນຈັດເກັບ + ຖ້າທ່ານບໍ່ສາມາດເຂົ້າເຖິງລະບົບທີ່ມີຢູ່ແລ້ວ + ໃຊ້ລະຫັດຜ່ານການກູ້ຂໍ້ມູນ ຫຼື ໃຊ້ກະເເຈ + ລຶບຂໍ້ມູນບັນຊີປະເພດ %1$s ບໍ\? +\n +\nໃຊ້ຢ່າງລະມັດລະວັງ, ມັນອາດຈະນໍາໄປສູ່ພຶດຕິກໍາທີ່ບໍ່ຄາດຄິດ. + ເຄື່ອງມືພັດທະນາ + ເປີດໂໝດຢູ່ໃນຍົນແລ້ວ + ຂາດການເຊື່ອມຕໍ່ກັບເຊີບເວີ + ບໍ່ + ແມ່ນແລ້ວ + ເກືອບຈະແລ້ວ! %s ສະແດງເຄື່ອງໝາຍຖືກບໍ\? + ລະຫັດ QR + ຕັ້ງຄ່າກະເເຈຄືນໃໝ່ + ເລີ່ມຕົ້ນ CrossSigning + ຂໍ້ຄວາມທີ່ສົ່ງໄປຫາ ແລະ ກັບຈະຕິດເຄື່ອງໝາຍໄວ້ຈົນກວາຜູ້ໃຊ້ນີ້ຈະໄວ້ວາງໃຈ. ອີກທາງເລືອກໜຶ່ງ, ທ່ານສາມາດກວດສອບດ້ວຍຕົນເອງ. + %1$s (%2$s) ເຂົ້າສູ່ລະບົບໂດຍໃຊ້ລະບົບໃໝ່: + ລະບົບນີ້ເຊື່ອຖືໄດ້ສໍາລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ ເພາະວ່າ %1$s (%2$s) ໄດ້ຢັ້ງຢືນແລ້ວ: + ເຊື່ອຖືບໍ່ໄດ້ + ເຊື່ອຖືໄດ້ + ລະບົບ + ການຮັບລະບົບບໍ່ສຳເລັດ + ຄຳເຕືອນ + ຢັ້ງຢືນແລ້ວ + ຢືນຢັນ + ໃຊ້ລະບົບທີ່ມີຢູ່ແລ້ວເພື່ອກວດສອບ, ໃຫ້ເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດ. + ຢືນຢັນການເຂົ້າສູ່ລະບົບນີ້ + + %dລະບົບທີ່ໃຊ້ງານຢູ່ + + ຢືນຢັນລະບົບນີ້ເພື່ອກຳນົດເຄື່ອງໝາຍທີ່ເຊື່ອຖືໄດ້ ແລະໃຫ້ສິດເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້. ຖ້າທ່ານບໍ່ໄດ້ເຂົ້າສູ່ລະບົບນີ້ ບັນຊີຂອງທ່ານອາດຈະຖືກໂຈມຕີ: + ລະບົບນີ້ຖືກເຊື່ອຖືໄດ້ສຳລັບການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ ເພາະວ່າທ່ານໄດ້ຢັ້ງຢືນແລ້ວ: + ບໍ່ມີຂໍ້ມູນການເຂົ້າລະຫັດລັບ + ບໍ່ໝັ້ນຄົງ + ໝັ້ນຄົງ + ເວີຊັນເລີ່ມຕົ້ນ + ເວີຊັ້ນຫ້ອງ 👓 + ບໍ່ຮູ້ຂອບເຂດຈໍາກັດ. + homeserver ຂອງທ່ານຍອມຮັບໄຟລ໌ແນບ (ໄຟລ໌, ມີເດຍ, ແລະ ອື່ນໆ) ທີ່ມີຂະຫນາດເຖິງ %s. + ຂີດຈຳກັດການອັບໂຫລດໄຟລ໌ເຊີບເວີ + ເວີຊັ້ນເຊີບເວີ + ຊື່ເຊີບເວີ + ອອກຈາກລະບົບນີ້ + ຈັດການ ລະບົບ + ສະແດງລະບົບທັງໝົດ + ລະບົບທີ່ໃຊ້ງານ + ຜູ້ເບິ່ງແຍງເຊີບເວີຂອງທ່ານໄດ້ປິດການນຳໃຊ້ການເຂົ້າລະຫັດແແຕ່ຕົ້ນທາງຮອດປາຍທາງໂດຍຄ່າເລີ່ມຕົ້ນໃນຫ້ອງສ່ວນຕົວ ແລະ ຂໍ້ຄວາມໂດຍກົງ. + Cross-Signing ບໍ່ໄດ້ເປີດໃຊ້ງານ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ. +\nກະເເຈຄີບໍ່ໜ້າເຊື່ອຖືໄດ້ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ +\nກະເເຈແມ່ນໜ້າເຊື່ອຖືໄດ້. +\nກະແຈສ່ວນຕົວບໍ່ຮູ້ຈັກ + Cross-Signing ແມ່ນເປີດໃຊ້ງານ +\nກະແຈສ່ວນຕົວຢູ່ໃນອຸປະກອນ. + Cross-Signing + ຕອນນີ້ລະບົບໃໝ່ຂອງທ່ານໄດ້ຮັບການຢັ້ງຢືນແລ້ວ.ມີການເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດຂອງທ່ານ, ແລະ ຜູ້ໃຊ້ອື່ນໆຈະເຫັນວ່າເປັນທີ່ເຊື່ອຖືໄດ້. + ຂໍ້ຄວາມຜູ້ໃຊ້ນີ້ແມ່ນເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ ແລະ ບໍ່ສາມາດອ່ານໄດ້ໂດຍພາກສ່ວນທີສາມ. + ເມື່ອເປີດໃຊ້ແລ້ວ, ການເຂົ້າລະຫັດລັບຂອງຫ້ອງບໍ່ສາມາດປິດໃຊ້ງານໄດ້. ຂໍ້ຄວາມທີ່ສົ່ງຢູ່ໃນຫ້ອງທີ່ເຂົ້າລະຫັດບໍ່ສາມາດເຫັນໄດ້ ເຊີບເວີບໍ່ສາມາດເບິ່ງເບິ່ງຂໍ້ຄວາມທີ່ສົ່ງໃນຫ້ອງທີ່ເຂົ້າລະຫັດ, ມີແຕ່ຜູ້ເຂົ້າຮ່ວມຂອງຫ້ອງເທົ່ານັ້ນ. ການເປີດໃຊ້ການເຂົ້າລະຫັດອາດຈະປ້ອງກັນບໍ່ໃຫ້ bots ແລະ bridgesຈໍານວນຫຼາຍເຮັດວຽກຢ່າງຖືກຕ້ອງ. + ປຽບທຽບລະຫັດທີ່ສະແດງຢູ່ໃນໜ້າຈໍຂອງຜູ້ໃຊ້ອື່ນ. + ປຽບທຽບ emoji ທີ່ເປັນເອກະລັກ, ໃຫ້ແນ່ໃຈວ່າພວກມັນປາກົດຢູ່ໃນລໍາດັບດຽວກັນ. + ເພື່ອຄວາມປອດໄພ, ດຳເນິນການແບບນີ້ດ້ວຍຕົນເອງ ຫຼື ໃຊ້ວິທີອື່ນເພື່ອຕິດຕໍ່ສື່ສານ. + ເພື່ອຄວາມປອດໄພ, ກະລຸນາຢັ້ງຢືນ %s ໂດຍການກວດສອບລະຫັດຄັ້ງດຽວ. + ເປີດໃຊ້ງານການເຂົ້າລະຫັດ + ເປີດໃຊ້ການເຂົ້າລະຫັດບໍ\? + ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເປີດໃຊ້ການເຂົ້າລະຫັດໃນຫ້ອງນີ້. + ເປີດການເຂົ້າລະຫັດແຕ່ຕົ້ນທາງເຖິງປາຍທາງ… + ບັນນາທິການຂໍ້ຄວາມ + ທາມລາຍ + ສົ່ງ emote ທີ່ກຳນົດເປັນສີຮຸ້ງ + ສົ່ງຂໍ້ຄວາມທີ່ກຳນົດເປັນສີຮຸ້ງ + ລະບົບນີ້ບໍ່ສາມາດແບ່ງປັນການຢືນຢັນນີ້ກັບລະບົບອື່ນຂອງທ່ານໄດ້. +\nການຢັ້ງຢືນຈະຖືກບັນທຶກໄວ້ໃນເຄື່ອງ ແລະ ແບ່ງປັນໃນເວີຊັນຂອງແອັບໃນອະນາຄົດ. + ບໍ່ສົນໃຈ + ${app_name} ປະສົບບັນຫາໃນເວລາສະແດງເນື້ອຫາຂອງເຫດການດ້ວຍ id \'%1$s\' + ${app_name} ບໍ່ໄດ້ຈັດການເຫດການຂອງປະເພດ \'%1$s\' + ໄປເພື່ອອ່ານ + ຂໍ້ຄວາມໂດຍກົງ + ກຳນົດເອງ (%1$d) ໃນ %2$s + ຄ່າເລີ່ມຕົ້ນໃນ %1$s + ຜູ້ຄວບຄຸມໃນ %1$s + ຜູ້ເບິ່ງແຍງລະບົບໃນ %1$s + ຜູ້ໃຊ້ + ເຊີນ + ກຳນົດເອງ + ຜູ້ຄວບຄຸມ + ຜູ້ເບິ່ງແຍງລະບົບ + ລົບລ້າງສີ nick + ກຳລັງອອກຈາກຫ້ອງ… + ອອກໄປ + ອອກຈາກຫ້ອງ + ອັບໂຫຼດ + ສິ່ງນີ້ຈະຖືກສະແດງເມື່ອທ່ານສົ່ງຂໍ້ຄວາມ. + ເລືອກຊື່ທີ່ຈະສະແດງ + ບັນຊີຂອງທ່ານ%sໄດ້ຖືກສ້າງຂື້ນແລ້ວ. + ຊົມເຊີຍ! + ພາຂ້ອຍກັບບ້ານ + ປັບແຕ່ງໂປຣໄຟລ໌ + ເຊື່ອມຕໍ່ກັບເຊີບເວີ + ຊອກການເຂົ້າຮ່ວມເຊີບເວີທີ່ມີຢູ່ແລ້ວບໍ\? + ຂ້າມຄໍາຖາມນີ້ + ບໍ່ແນ່ໃຈເທື່ອ\?ທາ່ນສາມາດ %s + ຊຸມຊົນ + ທີມງານ + ຫມູ່ເພື່ອນແລະຄອບຄົວ + ພວກເຮົາຈະຊ່ວຍໃຫ້ທ່ານເຊື່ອມຕໍ່ໄດ້. + ທ່ານຈະສົນທະນາກັບໃຜຫຼາຍທີ່ສຸດ\? + ${app_name} ແມ່ນດີຫຼາຍສຳລັບບ່ອນເຮັດວຽກ. ໄດ້ຮັບຄວາມໄວ້ວາງໃຈໂດຍອົງການຈັດຕັ້ງທີ່ປອດໄພທີ່ສຸດຂອງໂລກ. + ເຂົ້າລະຫັດແຕ່ຕົ້ນທາງຮອດປາຍທາງ ແລະ ບໍ່ຕ້ອງການເບີໂທລະສັບ. ບໍ່ມີການໂຄສະນາ ຫຼືການຂຸດຄົ້ນຂໍ້ມູນ. + ເລືອກບ່ອນທີ່ຈະເກັບການສົນທະນາຂອງທ່ານ, ໃຫ້ທ່ານຄວບຄຸມແລະເປັນເອກະລາດ. ເຊື່ອມຕໍ່ຜ່ານ Matrix. + ການສື່ສານທີ່ປອດໄພ ແລະ ເປັນເອກະລາດທີ່ເຮັດໃຫ້ທ່ານມີຄວາມເປັນສ່ວນຕົວໃນລະດັບດຽວກັນກັບການສົນທະນາແບບເຫັນໜ້າຢູ່ໃນເຮືອນຂອງທ່ານເອງ. + ຂໍ້ຄວາມສໍາລັບທີມງານຂອງທ່ານ. + ການສົ່ງຂໍ້ຄວາມທີ່ປອດໄພ. + ທ່ານຄວບຄຸມໄດ້. + ເປັນເຈົ້າຂອງບົດສົນທະນາຂອງທ່ານ. + ຂໍ້ຄວາມທີ່ຍັງບໍ່ໄດ້ອ່ານ + ທ່ານເປັນຜູ້ເຊີນນີ້ເທົ່ານັ້ນ. + %1$s ເຊີນນີ້ເທົ່ານັ້ນ. + ທ່ານໄດ້ເຊີນຫ້ອງເທົ່ານັ້ນ. + %1$s ກຳນົດຫ້ອງຖືກເຊີນເທົ່ານັ້ນ. + ທ່ານໄດ້ເປີດຫ້ອງສາທາລະນະໃຫ້ກັບທູຸກຄົນທີ່ຮູ້ຈັກລິ້ງ. + %1$s ໄດ້ກຳນົດຫ້ອງນີ້ໃຫ້ເປັນສາທາລະນະກັບກັບທຸກຄົນທີ່ຮູ້ຈັກລິ້ງ. + ລອງກົດໃສ່ຫ້ອງເພື່ອເບິ່ງຕົວເລືອກເພີ່ມເຕີມ + ທ່ານຍັງບໍ່ໄດ້ສົນໃຈຜູ້ໃຊ້ໃດໆ + ພິມຄໍາຫຼັກເພື່ອຊອກຫາການໂຕ້ຕອບ. + ຜູ້ເຮັດໃຫ້ເຊື່ອມເສຍ + ສົ່ງຂໍ້ຄວາມທີ່ໃຫ້ໄວ້ເປັນ spoiler + ທ່ານບໍ່ມີການປ່ຽນແປງ + %1$s ບໍ່ມີການປ່ຽນແປງ + ການຕັ້ງຄ່າຫ້ອງ + ອອກຈາກຫ້ອງ + ລຶບອອກຈາກບູລິມະສິດຕໍ່າ + ຕື່ມລຳດັບບຸລິມະສິດຕໍ່າ + ລຶບອອກຈາກລາຍການທີ່ມັກ + ເພີ່ມໃສ່ລາຍການທີ່ມັກ + ການຕັ້ງຄ່າ + ປິດສຽງ + ກ່າວເຖິງເທົ່ານັ້ນ + ຂໍ້ຄວາມທັງໝົດ + ຂໍ້ຄວາມທັງໝົດ (ສຽງດັງ) + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ເນື້ອຫານີ້ຖືກລາຍງານວ່າບໍ່ເໝາະສົມ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ລາຍງານວ່າບໍ່ເໝາະສົມ + ເນື້ອຫານີ້ຖືກລາຍງານວ່າເປັນສະແປມ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ລາຍງານວ່າເປັນ spam + ເນື້ອຫານີ້ໄດ້ຖືກລາຍງານ. +\n +\nຖ້າຫາກວ່າທ່ານບໍ່ຕ້ອງການທີ່ຈະເບິ່ງເນື້ອໃນເພີ່ມເຕີມຈາກຜູ້ໃຊ້ນີ້, ທ່ານສາມາດບໍ່ສົນໃຈໃຫ້ເຂົາເຈົ້າເຊື່ອງຂໍ້ຄວາມຂອງເຂົາເຈົ້າ. + ເນື້ອໃນທີ່ໄດ້ລາຍງານ + ບໍ່ສົນໃຈຜູ້ໃຊ້ + ລາຍງານ + ເຫດຜົນໃນການລາຍງານເນື້ອຫານີ້ + ລາຍງານເນື້ອຫານີ້ + ລາຍງານທີ່ກຳນົດເອງ… + ບໍ່ເຫມາະສົມ + diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml index 60426daa42..f308a32924 100644 --- a/vector/src/main/res/values-lv/strings.xml +++ b/vector/src/main/res/values-lv/strings.xml @@ -1526,7 +1526,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Turpināt Izvēlieties zvana signālu: Ienākoša zvana signāls - Izmantot noklusējuma $ {app_name} zvana signālu ienākošajiem zvaniem + Izmantot noklusējuma ${app_name} zvana signālu ienākošajiem zvaniem Pirms zvana uzsākšanas lūgt apstiprinājumu Novērst nejaušu zvanu SSL kļūda: Peer identitāte nav pārbaudīta. @@ -2102,4 +2102,4 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lai sūtītu balss ziņojumus, piešķiriet Mikrofona atļauju. Telpas Uztver paziņojumus - \ No newline at end of file + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 80f819157e..f567323fef 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1244,7 +1244,9 @@ \nOm te voorkomen dat hij/zij opnieuw toetreedt, kun je hem/haar ook verbannen. Reden voor verwijdering Weet u zeker dat u uitnodiging voor deze persoon wilt annuleren\? - Het niet meer negeren van deze persoon zal al zijn/haar berichten opnieuw doen weergeven. + Als u deze persoon niet negeert, worden alle berichten van deze persoon opnieuw weergegeven. +\n +\nHoud er rekening mee dat deze actie de app opnieuw zal starten en dat dit enige tijd kan duren. Door deze persoon te negeren worden zijn/haar berichten verwijderd uit gesprekken die jullie delen. \n \nU kunt deze actie op elk moment ongedaan maken in de algemene instellingen. @@ -2457,4 +2459,32 @@ %d server ACL\'s veranderen %d server ACL\'s wijzigingen + Live locatie laden… + 8 uur + 1 uur + 15 minuten + Deel uw live locatie voor + (%1$s) + %1$s (%2$s) + %1$s kan niet worden afgeluisterd + Pauze %1$s + Luister %1$s + %1$d minuten %2$d seconden + %1$s, %2$s, %3$s + Hun live locatie gedeeld + ${app_name} is ook geweldig voor op de werkplek. Het wordt vertrouwd door \'s werelds veiligste organisaties. + BÈTA + Discussies zijn werk in uitvoering met nieuwe, opwindende aankomende functies, zoals verbeterde meldingen. We horen graag uw feedback! + Discussies Beta-feedback + Geef feedback + BÈTA + Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de applicatie gebruikt. + Offline modus + Aanwezigheid + Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. %sWilt u toch discussies inschakelen\? + Discussies bèta + Discussies helpen uw gesprekken on-topic te houden en gemakkelijk bij te houden. %s Als u discussies inschakelt, wordt de app vernieuwd. Bij sommige accounts kan dit langer duren. + Discussies bèta + Leer meer + Probeer het uit \ No newline at end of file diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 9c1e9a9264..f6b714bdc2 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -1523,7 +1523,9 @@ Wykop użytkownika Czy na pewno anulować zaproszenie dla tego użytkownika\? Anuluj zaproszenie - Zaprzestanie ignorowania użytkownika spowoduje ponowne wyświetlanie wszystkich jego wiadomości. + Zaprzestanie ignorowania użytkownika spowoduje ponowne wyświetlanie wszystkich jego wiadomości. +\n +\nAplikacja uruchomi się ponownie aby zsynchronizować wiadomości, może to potrwać kilka chwil. Przestań ignorować użytkownika Zignorowanie tego użytkownika usunie wszystkie jego wiadomości z pokojów, które współdzielicie. \n @@ -2550,4 +2552,17 @@ Udostępnili swoją lokalizację Powiadom cały pokój Użytkownicy + BETA + Opinia o wątkach + Przekaż opinię + BETA + Jeśli włączone, inni zobaczą twój status jako offline, nawet gdy używasz aplikacji. + Tryb offline + Statusy + Twój serwer nie obsługuje obecnie wątków, co może powodować niestabilność tej funkcji. Niektóre wiadomości w wątkach mogą nie być poprawnie widoczne. %sCzy chcesz je włączyć pomimo tego\? + Wątki (Beta) + Dzięki wątkom Twoje rozmowy są zorganizowane i łatwe do śledzenia. %sWłączenie wątków uruchomi ponownie aplikację. Może to zająć więcej czasu dla niektórych kont. + Wątki (Beta) + Dowiedz się więcej + Wypróbuj \ No newline at end of file diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index f682a123f4..21ea05a47c 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -757,7 +757,9 @@ \n \nVocê pode reverter esta ação a qualquer momento nas configurações gerais. Designorar usuária(o) - Designorar esta(e) usuária(o) vai mostrar todas as mensagens dela(e) de novo. + Designorar esta(e) usuária(o) vai mostrar todas as mensagens dela(e) de novo. +\n +\nNote que esta ação vai recomeçar o app e pode levar algum tempo. Cancelar convite Você tem certeza que você quer cancelar o convite para esta(e) usuária(o)\? Expulsar usuária(o) @@ -2110,9 +2112,9 @@ Chamada de áudio perdida %d chamadas de áudio perdidas - Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão permanecer nesta sala arquivada. - Qualquer pessoa em um espaço pai vai ser capaz de achar e se juntar a esta sala - não precisa convidar manualmente todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. - Qualquer pessoa em %s vai ser capaz de achar e se juntar a esta sala - não precisa convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. + Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão ficar nesta sala arquivada. + Qualquer pessoa em um espaço pai vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. + Qualquer pessoa em %s vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora. Mensagem de Voz (%1$s) Não dá para responder ou editar enquanto mensagem de voz está ativa Não dá para gravar uma mensagem de voz @@ -2388,7 +2390,7 @@ Com quem você vai fazer chat mais\? Você já está visualizando esta thread! Visualizar Em Sala - Responder Em Thread + Responder em thread O comando \"%s\" é reconhecido mas não suportado em threads. De uma Thread Dica: Toque longo numa mensagem e use “%s”. @@ -2457,4 +2459,32 @@ Me leve para casa Personalizar perfil Desabilitar + Carregando localização ao vivo… + 8 horas + 1 hora + 15 minutos + Compartilhar sua localização ao vivo por + (%1$s) + %1$s (%2$s) + Incapaz de tocar %1$s + Pausar %1$s + Tocar %1$s + %1$d minutos %2$d segundos + %1$s, %2$s, %3$s + Compartilhou a localização ao vivo dela(e) + ${app_name} também é ótimo para o lugar de trabalho. Ele é confiado pelas organizações mais seguras do mundo. + BETA + Threads são um trabalho em progresso com novas funcionalidades próximas emocionantes, tais como notificações melhoradas. Nós adoraríamos ouvir seu feedback! + Feedback de Threads Beta + Dar Feedback + BETA + Se habilitado, você sempre vai aparecer offline para outras(os) usuárias(os), mesmo quando usando o aplicativo. + Modo offline + Presença + Seu servidorcasa não atualmente suporta threads, então esta funcionalidade pode ser inconfiável. Algumas mensagens de thread podem não estar confiavelmente disponíveis. %sVocê quer habilitar threads mesmo assim\? + Threads Beta + Threads ajudam manThreads ajudam manter suas conversas em-tópico e fáceis de rastrear. %sHabilitar threads vai refrescar o app. Isto pode tomar mais tempo para algumas contas. + Threads Beta + Saber mais + Teste aí \ No newline at end of file diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index ff1089caa0..a2c1404d2f 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -751,7 +751,9 @@ \n \nTúto akciu môžete kedykoľvek zmeniť späť vo všeobecných nastaveniach. Neignorovať používateľa - Zrušením ignorovania si opäť zobrazíte všetky správy od tohoto používateľa. + Zrušením ignorovania tohto používateľa sa opäť zobrazia všetky správy od neho. +\n +\nUpozorňujeme, že táto akcia spôsobí reštart aplikácie a môže chvíľu trvať. Zrušiť pozvanie Ste si istí, že chcete zrušiť pozvanie tohoto používateľa\? Vykázať používateľa @@ -2504,4 +2506,32 @@ Zober ma domov Prispôsobiť profil Zakázať + %1$s, %2$s, %3$s + Prítomnosť + Načítavanie polohy v reálnom čase… + Zdieľajte svoju polohu v reálnom čase na + 8 hodín + 1 hodinu + 15 minút + (%1$s) + %1$s (%2$s) + Nie je možné prehrať %1$s + Pozastaviť %1$s + Prehrať %1$s + %1$d minút %2$d sekúnd + Zdieľali svoju polohu v reálnom čase + ${app_name} je skvelý aj na pracovisku. Dôverujú mu najbezpečnejšie organizácie na svete. + BETA + Na vláknach sa neustále pracuje a pripravujú sa nové, zaujímavé funkcie, ako napríklad vylepšené oznámenia. Radi si vypočujeme vašu spätnú väzbu! + Spätná väzba na Beta vlákna + Poskytnite spätnú väzbu + BETA + Ak je táto funkcia zapnutá, budete sa ostatným používateľom vždy javiť ako offline, a to aj pri používaní aplikácie. + Režim offline + Váš domovský server v súčasnosti nepodporuje vlákna, takže táto funkcia môže byť nespoľahlivá. Niektoré správy vo vláknach nemusia byť spoľahlivo dostupné. %sChcete aj tak povoliť vlákna\? + Vlákna Beta + Vlákna pomáhajú udržiavať vaše konverzácie v téme a ľahké sledovanie. %sZapnutím vlákien sa aplikácia obnoví. V prípade niektorých účtov to môže trvať dlhšie. + Vlákna Beta + Zistiť viac + Vyskúšajte si to \ No newline at end of file diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index ddb492016c..18d2b137e1 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1528,7 +1528,9 @@ \n \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme. Hiqe shpërfilljen e përdoruesit - Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. + Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. +\n +\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe do të hajë ca kohë. Anuloje ftesën Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\? Përzëre përdoruesin @@ -2376,7 +2378,7 @@ Me kë do të bisedoni më shumë\? Po e shihni tashmë këtë rrjedhë! Shiheni në Dhomë - Përgjigjuni Te Rrjedha + Përgjigjuni te rrjedha Urdhri “%s” njihet, por nuk mbulohet në rrjedha. Nga një Rrjedhë Ndihmëz: Prekni pakëz gjatë një mesazh dhe përdorni “%s”. @@ -2445,4 +2447,30 @@ Shpjemëni në shtëpi Personalizoni profil Çaktivizoje + Përshtypje për Beta-n e Rrjedhave + Në u aktivizoftë, përdoruesve të tjerë do t’u dukeni në linjë, madje edhe kur përdoret aplikacioni. + Shërbyesi juaj Home aktualisht s’mbulon rrjedha, ndaj kjo veçori mund të jetë e paqëndrueshme. Disa mesazhe rrjedhash mund të mos jenë të përdorshëm. %sDoni të aktivizohen rrjedhat, sido qoftë\? + Beta Rrjedhash + Rrjedhat ju ndihmojnë t’i mbani bisedat tuaja brenda temës dhe ndjekjen e tyre të lehtë. %sAktivizimi i rrjedhave do të sjellë rifreskimin e aplikacionit. Për disa llogari, kjo mund të zgjasë më shumë. + Beta Rrjedhash + Rrjedhat janë në përpunim të vazhdueshëm, me veçori të reja, ngazëlluese, të ardhshme, bie fjala, njoftime. Do të donim fort të dëgjonim mendimin tuaj! + 8 orë + 1 orë + 15 minuta + Tregoni vendndodhjen tuaj drejtpërsëdrejti për + (%1$s) + %1$s (%2$s) + S’arrihet të luhet %1$s + Ndale %1$s + Luaje %1$s + %1$d minuta %2$d sekonda + %1$s, %2$s, %3$s + ${app_name} është gjithashtu i goditur për në punë. Është i besuar nga entet më të sigurta të botës. + BETA + Jepni Përshtypje + BETA + Mënyra offline + Prani + Mësoni më tepër + Provojeni \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index c6c8b45441..a7400528fa 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -837,7 +837,9 @@ Degradera Ignorera användare Avignorera användare - Att avignorera den här användaren kommer att visa alla meddelanden från hen igen. + Att avignorera den här användaren kommer att visa alla meddelanden från denne igen. +\n +\nObservera att detta kräver en omstart av appen och kan ta en stund. Avbryt inbjudan Är du säker på att du vill avbryta inbjudan för den här användaren\? Kicka användaren @@ -2421,14 +2423,14 @@ Trådar närmar sig beta 🎉 Stoppa - Kontinuerligt plats aktiverad - Om du vill dela din plats kontinuerligt så behöver ${app_name} åtkomst till din plats hela tiden när appen är i bakgrunden. + Plats i realtid aktiverad + Om du vill dela din plats i realtid så behöver ${app_name} åtkomst till din plats hela tiden när appen är i bakgrunden. \nVi kommer bara använda din plats under tiden du väljer. Tillåt åtkomst Dela den här platsen Dela den här platsen - Dela plats kontinuerligt - Dela plats kontinuerligt + Dela plats i realtid + Dela plats i realtid Dela min nuvarande plats Dela min nuvarande plats Zooma in till nuvarande plats @@ -2439,7 +2441,7 @@ \n \nDet kommer att vara en engångshändelse, eftersom trådar nu är en del av Matrixspecifikationen. Platsdelning pågår - ${app_name} Kontinuerlig plats + ${app_name} Realtidsplats Hemservern accepterar inte användarnamn med bara siffror. Hoppa över det här steget Spara och fortsätt @@ -2457,4 +2459,32 @@ För mig hem Anpassa profil Inaktivera + Laddar realtidsposition… + 8 timmar + 1 timme + 15 minuter + Dela din position i realtid under + (%1$s) + %1$s (%2$s) + Kunde inte spela %1$s + Pausa %1$s + Spela %1$s + %1$d minuter och %2$d sekunder + %1$s, %2$s, %3$s + Delade sin position i realtid + ${app_name} är även perfekt för arbetet. Flera av världens mest säkra organisationer litar på den. + BETA + Trådar är under utveckling med nya, spännande funktioner så som förbättrade aviseringar. Kom gärna med återkoppling! + Återkoppling för tråd-beta + Ge återkoppling + BETA + Med detta aktiverat så kommer du alltid se offline ut för andra, även om du är aktiv i appen. + Offline-läge + Närvaro + Din hemserver stödjer för närvarande inte trådar, så denna funktion kanske funkar dåligt. Vissa meddelanden i trådar kanske inte alltid visas. %sVill du ändå aktivera trådar\? + Tråd-beta + Trådar hjälper till att hålla dina konversationer till ämnet och gör dem lättare att följa. %sFör att aktivera trådar måste appen laddas om. Detta kan ta lång tid för vissa konton. + Trådar Beta + Läs mer + Prova \ No newline at end of file diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index ce1781cb9c..85900a392e 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -608,8 +608,8 @@ Почати відеозустріч У вас немає повноважень розпочати дзвінок У вас немає повноважень на дзвінок у цій кімнаті - У вас немає повноважень розпочати конференц-дзвінок - У вас немає повноважень розпочати конференц-дзвінок у цій кімнаті + У вас немає повноважень розпочати груповий виклик + У вас немає повноважень розпочати груповий виклик у цій кімнаті Скинути Відхилити Грати @@ -816,7 +816,7 @@ Заблокувати все Дозволити Цей віджет хоче використовувати такі ресурси: - На жаль, конференц-дзвінки з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0) + На жаль, групові виклики з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0) ID кімнати ID віджету Ваша тема @@ -1002,7 +1002,9 @@ Вилучити користувача Ви впевнені, що бажаєте скасувати запрошення для цього користувача\? Скасувати запрошення - Якщо перестати нехтувати цього користувача, усі його повідомлення стануть знову видимими. + Якщо перестати нехтувати цього користувача, усі його повідомлення стануть знову видимими. +\n +\nЗауважте, що ця дія перезапустить застосунок, а це може тривати деякий час. Рознехтувати користувача Нехтування цього користувача призведе до вилучення його повідомлень з усіх спільних кімнат. \n @@ -1142,7 +1144,7 @@ Вийти з кімнати Вийти з кімнати Вийти - Вийти з поточної конференції та перейти до іншої\? + Вийти з поточного групового виклику та перейти до іншого\? Це не загальнодоступна кімната. Ви не зможете знову приєднатися без запрошення. У вас немає дозволу на ввімкнення шифрування в цій кімнаті. Дозволи кімнати @@ -1461,7 +1463,7 @@ Використайте ключ відновлення, щоб розблокувати історію зашифрованих повідомлень використайте свій ключ відновлення Отримання резервної версії… - Перепрошуємо, під час спроби приєднатися до конференції сталася помилка + Перепрошуємо, під час спроби приєднатися до групового виклику сталася помилка Цей сервер уже є у списку Не вдалося знайти цей сервер або його список кімнат Введіть назву нового сервера, який потрібно дослідити. @@ -2549,4 +2551,32 @@ На головну Персоналізувати профіль Вимкнути + Завантаження місцеперебування наживо… + 8 годин + 1 годину + 15 хвилин + Ділитися своїм місцеперебуванням наживо + (%1$s) + %1$s (%2$s) + Неможливо відтворити %1$s + Зупинити %1$s + Відтворити %1$s + %1$d хвилин %2$d секунд + %1$s, %2$s, %3$s + Ділиться своїм місцеперебуванням наживо + ${app_name} також чудово підходить для роботи. Йому довіряють найтаємніші організації світу. + БЕТА + Треди — це поточна розробка з новими, захопливими майбутніми функціями, такими як вдосконалені сповіщення. Ми хотіли б почути ваш відгук! + Відгук про Треди бета + Надіслати відгук + БЕТА + Якщо ввімкнено, ви завжди будете видимі в режимі офлайн для інших користувачів, навіть коли користуєтеся застосунком. + Офлайн режим + Присутність + Ваш домашній сервер не підтримує тредів, тому ця функція може бути ненадійною. Деякі треди повідомлень можуть бути недоступними. %sУсе одно хочете ввімкнути треди\? + Треди бета + Треди допомагають упорядкувати свої розмови за темами та легко стежити за ними. %sУвімкнення тредів перезавантажить застосунок. Це може тривати більше часу для деяких облікових записів. + Треди бета + Докладніше + Спробувати \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index 67bf3f5e06..ec3045ec22 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -867,7 +867,9 @@ \n \n你随时可以在通用设置中反转此操作。 取消忽略用户 - 取消忽略此用户将重新显示来自他们的全部消息。 + 取消忽略此用户将重新显示来自他们的全部消息。 +\n +\n请注意:此操作将重新启动应用程序,并且可能需要一些时间。 取消邀请 你确定想要取消邀请此用户吗? 踢掉用户 @@ -2284,4 +2286,12 @@ 修改服务器 %d 的 ACLs + 子区有助于保持您的对话主题并易于跟踪 + 显示当前房间的所有子区 + 所有子区 + 筛选器 + 子区 + 在房间中筛选子区 + 复制子区的链接 + 在房间中查看 \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index ea0c677168..f93374acdb 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1501,7 +1501,9 @@ \n \n您隨時都可以在一般設定中撤銷此動作。 取消忽略使用者 - 取消忽略此使用者將再次顯示從他們而來的所有訊息。 + 取消忽略此使用者將再次顯示從他們而來的所有訊息。 +\n +\n注意,此動作將會重新啟動應用程式,且可能需要一些時間。 取消邀請 您確定您想要取消對此使用者的邀請嗎? 踢除使用者 @@ -2410,4 +2412,32 @@ 帶我回家 個人化檔案 停用 + 正在載入即時位置…… + 8小時 + 1小時 + 15分鐘 + 分享您的即時位置 + (%1$s) + %1$s (%2$s) + 無法播放 %1$s + 暫停 %1$s + 播放 %1$s + %1$d分鐘%2$d秒 + %1$s, %2$s, %3$s + 分享了他們的即時位置 + ${app_name} 也非常適合工作場所。其受到世界上最安全的組織信任。 + 測試版 + 討論串是一項正在進行中的工作,包含了新的、令人興奮的即將推出的功能,例如改進的通知。我們想要聽到您的回饋! + 討論串測試版回饋 + 給予回饋 + 測試版 + 若啟用,即使在使用應用程式時,您也會對其他使用者顯示為離線狀態。 + 離線模式 + 在場 + 您的家伺服器目前不支援討論串,所以此功能可能不可靠。部份已進入討論串的訊息可能無法可靠地使用。%s您仍想啟用討論串嗎? + 討論串測試版 + 討論串有助於讓您的對話不離題且易於追蹤。%s啟用討論串將會重新整理應用程式。對於特定帳號,可能需要更長的時間。 + 討論串測試版 + 取得更多資訊 + 試試看 \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8607e8e0be..e6568975e2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - %s\'s invitation Your invitation @@ -1058,7 +1057,7 @@ Play shutter sound - Flair + Flair 3 days @@ -2179,7 +2178,7 @@ Leave "Leaving the room…" - Override nick color + Override display name color Admins Moderators diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 0c50083d53..02dc36bf39 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -8,12 +8,6 @@ app:fragment="im.vector.app.features.settings.VectorSettingsGeneralFragment" app:isPreferenceVisible="@bool/settings_root_general_visible" /> - - - \ No newline at end of file + diff --git a/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt index 7562dfdf14..abd730707d 100644 --- a/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt @@ -52,10 +52,12 @@ class SharedSecureStorageViewModelTest { val viewModel = createViewModel() viewModel .test() - .assertState(aViewState( - hasPassphrase = true, - step = SharedSecureStorageViewState.Step.EnterPassphrase - )) + .assertState( + aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase + ) + ) .finish() } @@ -67,10 +69,12 @@ class SharedSecureStorageViewModelTest { viewModel .test() - .assertState(aViewState( - hasPassphrase = false, - step = SharedSecureStorageViewState.Step.EnterKey - )) + .assertState( + aViewState( + hasPassphrase = false, + step = SharedSecureStorageViewState.Step.EnterKey + ) + ) .finish() } diff --git a/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt index bb05357cb2..d45e6e7ce1 100644 --- a/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCaseTest.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.utils.saveMedia import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.test.fakes.FakeClock import im.vector.app.test.fakes.FakeFile import im.vector.app.test.fakes.FakeSession import io.mockk.MockKAnnotations @@ -57,6 +58,8 @@ class DownloadMediaUseCaseTest { @MockK lateinit var notificationUtils: NotificationUtils + private val clock = FakeClock() + private val file = FakeFile() @OverrideMockKs @@ -85,7 +88,8 @@ class DownloadMediaUseCaseTest { every { getMimeTypeFromUri(appContext, uri) } returns mimeType file.givenName(name) file.givenUri(uri) - coEvery { saveMedia(any(), any(), any(), any(), any()) } just runs + clock.givenEpoch(123) + coEvery { saveMedia(any(), any(), any(), any(), any(), any()) } just runs // When val result = downloadMediaUseCase.execute(file.instance) @@ -100,7 +104,7 @@ class DownloadMediaUseCaseTest { getMimeTypeFromUri(appContext, uri) } coVerify { - saveMedia(appContext, file.instance, name, mimeType, notificationUtils) + saveMedia(appContext, file.instance, name, mimeType, notificationUtils, 123) } } @@ -113,8 +117,9 @@ class DownloadMediaUseCaseTest { val error = Throwable() file.givenName(name) file.givenUri(uri) + clock.givenEpoch(345) every { getMimeTypeFromUri(appContext, uri) } returns mimeType - coEvery { saveMedia(any(), any(), any(), any(), any()) } throws error + coEvery { saveMedia(any(), any(), any(), any(), any(), any()) } throws error // When val result = downloadMediaUseCase.execute(file.instance) @@ -129,7 +134,7 @@ class DownloadMediaUseCaseTest { getMimeTypeFromUri(appContext, uri) } coVerify { - saveMedia(appContext, file.instance, name, mimeType, notificationUtils) + saveMedia(appContext, file.instance, name, mimeType, notificationUtils, 345) } } } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt index 3429f882f8..7499a2feae 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt @@ -28,12 +28,14 @@ class NotificationEventQueueTest { @Test fun `given events when redacting some then marks matching event ids as redacted`() { - val queue = givenQueue(listOf( - aSimpleNotifiableEvent(eventId = "redacted-id-1"), - aNotifiableMessageEvent(eventId = "redacted-id-2"), - anInviteNotifiableEvent(eventId = "redacted-id-3"), - aSimpleNotifiableEvent(eventId = "kept-id"), - )) + val queue = givenQueue( + listOf( + aSimpleNotifiableEvent(eventId = "redacted-id-1"), + aNotifiableMessageEvent(eventId = "redacted-id-2"), + anInviteNotifiableEvent(eventId = "redacted-id-3"), + aSimpleNotifiableEvent(eventId = "kept-id"), + ) + ) queue.markRedacted(listOf("redacted-id-1", "redacted-id-2", "redacted-id-3")) @@ -77,10 +79,12 @@ class NotificationEventQueueTest { @Test fun `given events when syncing without rooms left or joined ids then does not change the events`() { - val queue = givenQueue(listOf( - aNotifiableMessageEvent(roomId = "a-room-id"), - anInviteNotifiableEvent(roomId = "a-room-id") - )) + val queue = givenQueue( + listOf( + aNotifiableMessageEvent(roomId = "a-room-id"), + anInviteNotifiableEvent(roomId = "a-room-id") + ) + ) queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = emptyList()) @@ -145,7 +149,7 @@ class NotificationEventQueueTest { @Test fun `given replaceable event when adding event with same id then updates existing event`() { val replaceableEvent = aSimpleNotifiableEvent(canBeReplaced = true) - val updatedEvent = replaceableEvent.copy(title = "updated title") + val updatedEvent = replaceableEvent.copy(title = "updated title", isUpdated = true) val queue = givenQueue(listOf(replaceableEvent)) queue.add(updatedEvent) @@ -167,7 +171,7 @@ class NotificationEventQueueTest { @Test fun `given event when adding new event with edited event id matching the existing event id then updates existing event`() { val editedEvent = aSimpleNotifiableEvent(eventId = "id-to-edit") - val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title") + val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title", isUpdated = true) val queue = givenQueue(listOf(editedEvent)) queue.add(updatedEvent) @@ -178,7 +182,7 @@ class NotificationEventQueueTest { @Test fun `given event when adding new event with edited event id matching the existing event edited id then updates existing event`() { val editedEvent = aSimpleNotifiableEvent(eventId = "0", editedEventId = "id-to-edit") - val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title") + val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title", isUpdated = true) val queue = givenQueue(listOf(editedEvent)) queue.add(updatedEvent) @@ -189,10 +193,12 @@ class NotificationEventQueueTest { @Test fun `when clearing membership notification then removes invite events with matching room id`() { val roomId = "a-room-id" - val queue = givenQueue(listOf( - anInviteNotifiableEvent(roomId = roomId), - aNotifiableMessageEvent(roomId = roomId) - )) + val queue = givenQueue( + listOf( + anInviteNotifiableEvent(roomId = roomId), + aNotifiableMessageEvent(roomId = roomId) + ) + ) queue.clearMemberShipNotificationForRoom(roomId) @@ -202,10 +208,12 @@ class NotificationEventQueueTest { @Test fun `when clearing messages for room then removes message events with matching room id`() { val roomId = "a-room-id" - val queue = givenQueue(listOf( - anInviteNotifiableEvent(roomId = roomId), - aNotifiableMessageEvent(roomId = roomId) - )) + val queue = givenQueue( + listOf( + anInviteNotifiableEvent(roomId = roomId), + aNotifiableMessageEvent(roomId = roomId) + ) + ) queue.clearMessagesForRoom(roomId) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index f0f9a4dbc7..e88f01d4e3 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -54,14 +54,16 @@ class NotificationFactoryTest { val result = roomInvitation.toNotifications(MY_USER_ID) - result shouldBeEqualTo listOf(OneShotNotification.Append( - notification = expectedNotification, - meta = OneShotNotification.Append.Meta( - key = A_ROOM_ID, - summaryLine = AN_INVITATION_EVENT.description, - isNoisy = AN_INVITATION_EVENT.noisy, - timestamp = AN_INVITATION_EVENT.timestamp - )) + result shouldBeEqualTo listOf( + OneShotNotification.Append( + notification = expectedNotification, + meta = OneShotNotification.Append.Meta( + key = A_ROOM_ID, + summaryLine = AN_INVITATION_EVENT.description, + isNoisy = AN_INVITATION_EVENT.noisy, + timestamp = AN_INVITATION_EVENT.timestamp + ) + ) ) } @@ -71,9 +73,11 @@ class NotificationFactoryTest { val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) - result shouldBeEqualTo listOf(OneShotNotification.Removed( - key = A_ROOM_ID - )) + result shouldBeEqualTo listOf( + OneShotNotification.Removed( + key = A_ROOM_ID + ) + ) } @Test @@ -83,14 +87,16 @@ class NotificationFactoryTest { val result = roomInvitation.toNotifications(MY_USER_ID) - result shouldBeEqualTo listOf(OneShotNotification.Append( - notification = expectedNotification, - meta = OneShotNotification.Append.Meta( - key = AN_EVENT_ID, - summaryLine = A_SIMPLE_EVENT.description, - isNoisy = A_SIMPLE_EVENT.noisy, - timestamp = AN_INVITATION_EVENT.timestamp - )) + result shouldBeEqualTo listOf( + OneShotNotification.Append( + notification = expectedNotification, + meta = OneShotNotification.Append.Meta( + key = AN_EVENT_ID, + summaryLine = A_SIMPLE_EVENT.description, + isNoisy = A_SIMPLE_EVENT.noisy, + timestamp = AN_INVITATION_EVENT.timestamp + ) + ) ) } @@ -100,9 +106,11 @@ class NotificationFactoryTest { val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) - result shouldBeEqualTo listOf(OneShotNotification.Removed( - key = AN_EVENT_ID - )) + result shouldBeEqualTo listOf( + OneShotNotification.Removed( + key = AN_EVENT_ID + ) + ) } @Test @@ -123,9 +131,11 @@ class NotificationFactoryTest { val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) - result shouldBeEqualTo listOf(RoomNotification.Removed( - roomId = A_ROOM_ID - )) + result shouldBeEqualTo listOf( + RoomNotification.Removed( + roomId = A_ROOM_ID + ) + ) } @Test @@ -134,17 +144,23 @@ class NotificationFactoryTest { val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) - result shouldBeEqualTo listOf(RoomNotification.Removed( - roomId = A_ROOM_ID - )) + result shouldBeEqualTo listOf( + RoomNotification.Removed( + roomId = A_ROOM_ID + ) + ) } @Test - fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(notificationFactory) { - val roomWithRedactedMessage = mapOf(A_ROOM_ID to listOf( - ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)), - ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(eventId = "not-redacted")) - )) + fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith( + notificationFactory + ) { + val roomWithRedactedMessage = mapOf( + A_ROOM_ID to listOf( + ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)), + ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(eventId = "not-redacted")) + ) + ) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = "not-redacted")) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(withRedactedRemoved, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index 6dca0479e7..7bfdfdc40c 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -88,10 +88,14 @@ class NotificationRendererTest { @Test fun `given a room message group notification is added when rendering then show the message notification and update summary`() { - givenNotifications(roomNotifications = listOf(RoomNotification.Message( - A_NOTIFICATION, - MESSAGE_META - ))) + givenNotifications( + roomNotifications = listOf( + RoomNotification.Message( + A_NOTIFICATION, + MESSAGE_META + ) + ) + ) renderEventsAsNotifications() @@ -127,10 +131,14 @@ class NotificationRendererTest { @Test fun `given a simple notification is added when rendering then show the simple notification and update summary`() { - givenNotifications(simpleNotifications = listOf(OneShotNotification.Append( - A_NOTIFICATION, - ONE_SHOT_META.copy(key = AN_EVENT_ID) - ))) + givenNotifications( + simpleNotifications = listOf( + OneShotNotification.Append( + A_NOTIFICATION, + ONE_SHOT_META.copy(key = AN_EVENT_ID) + ) + ) + ) renderEventsAsNotifications() @@ -166,10 +174,14 @@ class NotificationRendererTest { @Test fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() { - givenNotifications(simpleNotifications = listOf(OneShotNotification.Append( - A_NOTIFICATION, - ONE_SHOT_META.copy(key = A_ROOM_ID) - ))) + givenNotifications( + simpleNotifications = listOf( + OneShotNotification.Append( + A_NOTIFICATION, + ONE_SHOT_META.copy(key = A_ROOM_ID) + ) + ) + ) renderEventsAsNotifications() diff --git a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt index 5a3c323316..6021b755f4 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt @@ -65,7 +65,11 @@ class DirectLoginUseCaseTest { @Test fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest { - fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT) + fakeAuthenticationService.givenWellKnown( + A_LOGIN_OR_REGISTER_ACTION.username, + config = NO_HOMESERVER_CONFIG, + result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT + ) val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) @@ -76,7 +80,11 @@ class DirectLoginUseCaseTest { @Test fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest { - fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT) + fakeAuthenticationService.givenWellKnown( + A_LOGIN_OR_REGISTER_ACTION.username, + config = NO_HOMESERVER_CONFIG, + result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT + ) val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 62fc9548b2..c26c73a9a7 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -105,7 +105,14 @@ class OnboardingViewModelTest { @Test fun `given supports changing display name, when handling PersonalizeProfile, then emits contents choose display name`() = runTest { - viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false))) + viewModelWith( + initialState.copy( + personalizationState = PersonalizationState( + supportsChangingDisplayName = true, + supportsChangingProfilePicture = false + ) + ) + ) val test = viewModel.test() viewModel.handle(OnboardingAction.PersonalizeProfile) @@ -117,7 +124,14 @@ class OnboardingViewModelTest { @Test fun `given only supports changing profile picture, when handling PersonalizeProfile, then emits contents choose profile picture`() = runTest { - viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true))) + viewModelWith( + initialState.copy( + personalizationState = PersonalizationState( + supportsChangingDisplayName = false, + supportsChangingProfilePicture = true + ) + ) + ) val test = viewModel.test() viewModel.handle(OnboardingAction.PersonalizeProfile) @@ -473,10 +487,12 @@ class OnboardingViewModelTest { private fun givenSuccessfulRegistrationForStartAndDummySteps(missingStages: List) { val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList()) - givenRegistrationResultsFor(listOf( - A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult), - RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession) - )) + givenRegistrationResultsFor( + listOf( + A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult), + RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession) + ) + ) givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt similarity index 67% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt index 8c1bdf6768..1d531f147f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeClock.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.model +package im.vector.app.test.fakes -enum class OutgoingGossipingRequestState { - UNSENT, - SENDING, - SENT, - CANCELLING, - CANCELLED, - FAILED_TO_SEND, - FAILED_TO_CANCEL +import im.vector.app.core.time.Clock +import io.mockk.every +import io.mockk.mockk + +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt index 4f349f8506..d7d18b925d 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt @@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService { override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) = integrityResult - override fun requestSecret(name: String, myOtherDeviceId: String) { + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { TODO("Not yet implemented") } }