diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..0542767eff --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 8efd6cd8f6..98df9213cf 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -17,7 +17,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.1.2 + uses: danger/danger-js@11.1.3 with: args: "--dangerfile tools/danger/dangerfile.js" env: diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index ced52e087a..ee19f55a73 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -31,7 +31,7 @@ jobs: ui-tests: name: UI Tests (Synapse) needs: should-i-run - runs-on: macos-latest + runs-on: buildjet-4vcpu-ubuntu-2204 strategy: fail-fast: false matrix: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ae8113efce..87cd2d76c1 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -72,7 +72,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.1.2 + uses: danger/danger-js@11.1.3 with: args: "--dangerfile tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2dc8af70cb..9aa6f78129 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,12 +8,15 @@ on: # Enrich gradle.properties for CI/CD env: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon jobs: tests: name: Runs all tests - runs-on: macos-latest # for the emulator + runs-on: buildjet-4vcpu-ubuntu-2204 + strategy: + matrix: + api-level: [28] # Allow all jobs on main and develop. Just one per PR. concurrency: group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} @@ -21,12 +24,30 @@ jobs: steps: - uses: actions/checkout@v3 with: + lfs: true fetch-depth: 0 - uses: actions/setup-java@v3 with: distribution: 'adopt' java-version: '11' - uses: gradle/gradle-build-action@v2 + with: + cache-read-only: ${{ github.ref != 'refs/heads/develop' }} + gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/develop' }} + + # tchap : remove verifyScreenshots + #- name: Run screenshot tests + # run: ./gradlew verifyScreenshots $CI_GRADLE_ARG_PROPERTIES + + - name: Archive Screenshot Results on Error + if: failure() + uses: actions/upload-artifact@v3 + with: + name: screenshot-results + path: | + **/out/failures/ + **/build/reports/tests/*UnitTest/ + - uses: actions/setup-python@v4 with: python-version: 3.8 @@ -36,45 +57,51 @@ jobs: httpPort: 8080 disableRateLimiting: true public_baseurl: "http://10.0.2.2:8080/" + - name: Run all the codecoverage tests at once - id: tests uses: reactivecircus/android-emulator-runner@v2 - continue-on-error: true + # continue-on-error: true with: - api-level: 28 + api-level: ${{ matrix.api-level }} arch: x86 profile: Nexus 5X + target: playstore force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - emulator-build: 7425822 + # emulator-build: 7425822 script: | # Tchap : comment this task because CI failed # ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES - # NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure' - - name: Run all the codecoverage tests at once (retry if emulator failed) - uses: reactivecircus/android-emulator-runner@v2 - if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded. + # NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves steps.tests.outcome = 'failure' + ### - name: Run all the codecoverage tests at once (retry if emulator failed) + ### uses: reactivecircus/android-emulator-runner@v2 + ### if: always() && steps.tests.outcome == 'failure' # don't run if previous step succeeded. + ### with: + ### api-level: 28 + ### arch: x86 + ### profile: Nexus 5X + ### force-avd-creation: false + ### emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + ### disable-animations: true + ### emulator-build: 7425822 + ### script: | + ### ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES + ### ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES + + - name: Upload Integration Test Report Log + uses: actions/upload-artifact@v3 + if: always() with: - api-level: 28 - arch: x86 - profile: Nexus 5X - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - emulator-build: 7425822 - script: | - # Tchap : comment this task because CI failed - # ./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES - ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES - ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES - ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES - - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES - # Tchap: Skip in forks - if: github.repository == 'vector-im/element-android' && always() # we may have failed a previous step and retried, that's OK + name: integration-test-error-results + path: | + */build/outputs/androidTest-results/connected/ + */build/reports/androidTests/connected/ # we may have failed a previous step and retried, that's OK - name: Publish results to Sonar diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml new file mode 100644 index 0000000000..203ecb0481 --- /dev/null +++ b/.github/workflows/validate-lfs.yml @@ -0,0 +1,15 @@ +name: Validate Git LFS + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + name: Validate + steps: + - uses: actions/checkout@v3 + with: + lfs: 'true' + + - run: | + ./tools/validate_lfs.sh diff --git a/.gitignore b/.gitignore index f1c0b99b58..aae906afc2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ /package.json /yarn.lock /node_modules +**/out/failures diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 108fa46424..e7d6648070 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,28 @@ +# Contributing to Element Android + + + +* [Android Studio settings](#android-studio-settings) + * [Template](#template) +* [Compilation](#compilation) +* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue) + * [Kotlin](#kotlin) + * [Tchap changes](#tchap-changes) + * [Changelog](#changelog) + * [Code quality](#code-quality) + * [Internal tool](#internal-tool) + * [ktlint](#ktlint) + * [lint](#lint) + * [Unit tests](#unit-tests) + * [Tests](#tests) + * [Internationalisation](#internationalisation) + * [Accessibility](#accessibility) + * [Layout](#layout) + * [Authors](#authors) +* [Thanks](#thanks) + + + ## Android Studio settings Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). diff --git a/ELEMENT_CHANGES.md b/ELEMENT_CHANGES.md index 518bbd8b67..d1e4834988 100644 --- a/ELEMENT_CHANGES.md +++ b/ELEMENT_CHANGES.md @@ -1,3 +1,104 @@ +Changes in Element v1.5.2 (2022-10-05) +====================================== + +Features ✨ +---------- + - New App Layout is now enabled by default! Go to the Settings > Labs to toggle this ([#7166](https://github.com/vector-im/element-android/issues/7166)) + - Render inline images in the timeline ([#351](https://github.com/vector-im/element-android/issues/351)) + - Add privacy setting to disable personalized learning by the keyboard ([#6633](https://github.com/vector-im/element-android/issues/6633)) + +Bugfixes 🐛 +---------- + - Disable emoji keyboard not applies in reply ([#5029](https://github.com/vector-im/element-android/issues/5029)) + - Fix animated images not autoplaying sometimes if only a thumbnail was fetched from the server ([#6215](https://github.com/vector-im/element-android/issues/6215)) + - Add Warning shield when a user previously verified rotated their cross signing keys ([#6702](https://github.com/vector-im/element-android/issues/6702)) + - Can't verify user when option to send keys to verified devices only is selected ([#6723](https://github.com/vector-im/element-android/issues/6723)) + - Add option to only send to verified devices per room (web parity) ([#6725](https://github.com/vector-im/element-android/issues/6725)) + - Delete pin code key and the key used for biometrics authentication on logout ([#6906](https://github.com/vector-im/element-android/issues/6906)) + - Fix crash on previewing images to upload on Android Pie. ([#7184](https://github.com/vector-im/element-android/issues/7184)) + - Fix app restarts in loop on Android 13 on the first run of the app. ([#7224](https://github.com/vector-im/element-android/issues/7224)) + +In development 🚧 +---------------- + - [Device Management] Learn more bottom sheets ([#7100](https://github.com/vector-im/element-android/issues/7100)) + - [Device management] Verify current session ([#7114](https://github.com/vector-im/element-android/issues/7114)) + - [Device management] Verify another session ([#7143](https://github.com/vector-im/element-android/issues/7143)) + - [Device management] Rename a session ([#7158](https://github.com/vector-im/element-android/issues/7158)) + - [Device Manager] Unverified and inactive sessions list ([#7170](https://github.com/vector-im/element-android/issues/7170)) + - [Device management] Sign out a session ([#7190](https://github.com/vector-im/element-android/issues/7190)) + - [Device Manager] Parse user agents ([#7247](https://github.com/vector-im/element-android/issues/7247)) + - [Voice Broadcast] Add a feature flag with the composer action ([#7258](https://github.com/vector-im/element-android/issues/7258)) + +Improved Documentation 📚 +------------------------ + - Draft onboarding documentation of the project at `./docs/_developer_onboarding.md` ([#7126](https://github.com/vector-im/element-android/issues/7126)) + +SDK API changes ⚠️ +------------------ + - Allow the sync timeout to be configured (mainly useful for testing) ([#7198](https://github.com/vector-im/element-android/issues/7198)) + - Ports SDK instrumentation tests to use suspending functions instead of countdown latches ([#7207](https://github.com/vector-im/element-android/issues/7207)) + - [Device Manager] Extend user agent to include device information ([#7209](https://github.com/vector-im/element-android/issues/7209)) + +Other changes +------------- + - Add support for `/tableflip` command ([#12](https://github.com/vector-im/element-android/issues/12)) + - Decreases the size of rounded corners and increases the maximum width of message bubbles to help avoid unnecessary unused space on screen ([#5712](https://github.com/vector-im/element-android/issues/5712)) + - Adds screenshot testing tooling ([#5798](https://github.com/vector-im/element-android/issues/5798)) + - [AppLayout]: added tracking of new analytics events ([#6508](https://github.com/vector-im/element-android/issues/6508)) + - Target API 12 and compile with Android SDK 32. ([#6929](https://github.com/vector-im/element-android/issues/6929)) + - Add basic integration of Sentry to capture errors and crashes if user has given consent. ([#7076](https://github.com/vector-im/element-android/issues/7076)) + - Add support to `/devtools` command. ([#7126](https://github.com/vector-im/element-android/issues/7126)) + - Fix lint warning, and cleanup the code ([#7159](https://github.com/vector-im/element-android/issues/7159)) + - Mutualize the pending auth handling ([#7193](https://github.com/vector-im/element-android/issues/7193)) + - CI: Prevent modification of translations by developer. ([#7211](https://github.com/vector-im/element-android/issues/7211)) + - Fix typo in strings.xml and make sure this is American English. ([#7287](https://github.com/vector-im/element-android/issues/7287)) + + +Changes in Element v1.5.1 (2022-09-28) +====================================== + +Security ⚠️ +---------- + +This update provides important security fixes, update now. +Ref: CVE-2022-39246 CVE-2022-39248 + +Changes in Element v1.5.0 (2022-09-23) +====================================== + +Features ✨ +---------- + - Deferred DMs - Enable and move the feature to labs settings ([#7180](https://github.com/vector-im/element-android/issues/7180)) + +Bugfixes 🐛 +---------- + - Fix text margin in QR code view when no display name is set ([#5424](https://github.com/vector-im/element-android/issues/5424)) + - [App Layout] Recents carousel now scrolled to first position when new item added to or moved to this position ([#6776](https://github.com/vector-im/element-android/issues/6776)) + - Fixed problem when room list's scroll did jump after rooms placeholders were replaced with rooms summary items ([#7079](https://github.com/vector-im/element-android/issues/7079)) + - Fixes crash when quickly double clicking FABs in the new app layout ([#7102](https://github.com/vector-im/element-android/issues/7102)) + - Fixes space list and new chat bottom sheets showing too small in New App Layout (especially evident in landscape) ([#7103](https://github.com/vector-im/element-android/issues/7103)) + - [App Layout] Room leaving prompt dialog now waits user to confirm leaving before do so ([#7122](https://github.com/vector-im/element-android/issues/7122)) + - Fix empty verification bottom sheet. ([#7130](https://github.com/vector-im/element-android/issues/7130)) + - [New Layout] Fixes new chat dialog not getting dismissed after selecting its actions ([#7132](https://github.com/vector-im/element-android/issues/7132)) + - Fixes Room List not getting updated when fragment is not in focus ([#7186](https://github.com/vector-im/element-android/issues/7186)) + +In development 🚧 +---------------- + - Create DM room only on first message - Add a spinner when sending the first message ([#6970](https://github.com/vector-im/element-android/issues/6970)) + - [Device Manager] Filter Other Sessions ([#7045](https://github.com/vector-im/element-android/issues/7045)) + - [Device management] Session details screen ([#7077](https://github.com/vector-im/element-android/issues/7077)) + - Create DM room only on first message - Fix glitch in the room list ([#7121](https://github.com/vector-im/element-android/issues/7121)) + - Create DM room only on first message - Handle the local rooms within the new AppLayout ([#7153](https://github.com/vector-im/element-android/issues/7153)) + +Other changes +------------- + - [Modules] Lifts the application variants to the app module ([#6779](https://github.com/vector-im/element-android/issues/6779)) + - Ensure that we do not expect all the Event fields when requesting `rooms/{roomId}/hierarchy` endpoint. ([#7035](https://github.com/vector-im/element-android/issues/7035)) + - Move some GitHub actions to buildjet runners, and remove the second attempt to run integration tests. ([#7108](https://github.com/vector-im/element-android/issues/7108)) + - Exclude legacy android support annotation library ([#7140](https://github.com/vector-im/element-android/issues/7140)) + - Pulling no longer hosted im.dlg:android-dialer directly into the repository and removing legacy support library usages ([#7142](https://github.com/vector-im/element-android/issues/7142)) + - Fixing build cache misses when compiling the vector module ([#7157](https://github.com/vector-im/element-android/issues/7157)) + Changes in Element v1.4.36 (2022-09-10) ======================================= diff --git a/Gemfile.lock b/Gemfile.lock index 90e846860e..276f4ae66a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,30 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) + CFPropertyList (3.0.5) + rexml addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) artifactory (3.0.15) atomos (0.1.3) - aws-eventstream (1.1.1) - aws-partitions (1.479.0) - aws-sdk-core (3.117.0) + aws-eventstream (1.2.0) + aws-partitions (1.619.0) + aws-sdk-core (3.132.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.44.0) - aws-sdk-core (~> 3, >= 3.112.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.58.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.96.1) - aws-sdk-core (~> 3, >= 3.112.0) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.4) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) + claide (1.1.0) claide-plugins (0.9.2) cork nap @@ -48,22 +49,24 @@ GEM octokit (~> 4.7) terminal-table (>= 1, < 4) declarative (0.0.20) - digest-crc (0.6.3) + digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) - emoji_regex (3.2.2) - excon (0.85.0) - faraday (1.5.1) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.92.4) + faraday (1.10.1) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) @@ -71,18 +74,22 @@ GEM faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) - faraday-http-cache (2.4.0) + faraday-http-cache (2.4.1) faraday (>= 0.8) faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) - faraday_middleware (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.4) - fastlane (2.187.0) + fastimage (2.2.6) + fastlane (2.209.0) CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.0.0) + addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) @@ -97,7 +104,7 @@ GEM faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.1) + google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) @@ -106,6 +113,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) naturally (~> 2.2) + optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -121,9 +129,9 @@ GEM gh_inspector (1.1.3) git (1.11.0) rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.8.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.0) + google-apis-androidpublisher_v3 (0.25.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-core (0.7.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -132,47 +140,47 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.6.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.6.0) - google-apis-core (>= 0.4, < 2.a) + google-apis-iamcredentials_v1 (0.13.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-playcustomapp_v1 (0.10.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-storage_v1 (0.17.0) + google-apis-core (>= 0.7, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.1.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.38.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.17.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.16.2) - faraday (>= 0.17.3, < 2.0) + googleauth (1.2.0) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.14) + signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.4.0) - json (2.5.1) - jwt (2.2.3) + jmespath (1.6.1) + json (2.6.2) + jwt (2.4.1) kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.1.0) + mini_mime (1.1.2) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) @@ -183,12 +191,13 @@ GEM faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) - os (1.1.1) + optparse (0.1.1) + os (1.1.4) plist (3.6.0) - public_suffix (4.0.6) + public_suffix (4.0.7) rake (13.0.6) rchardet (1.8.0) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -201,9 +210,9 @@ GEM addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) security (0.1.3) - signet (0.15.0) - addressable (~> 2.3) - faraday (>= 0.17.3, < 2.0) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -212,7 +221,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -220,11 +229,11 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.7) - unicode-display_width (1.7.0) + unf_ext (0.0.8.2) + unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcodeproj (1.20.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -239,6 +248,7 @@ GEM PLATFORMS universal-darwin-21 x86_64-darwin-20 + x86_64-linux DEPENDENCIES danger diff --git a/README.md b/README.md index 848ba366e3..093f690aaa 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,15 @@ If you would like to receive releases more quickly (bearing in mind that they ma ## Contributing Please refer to [CONTRIBUTING.md](https://github.com/tchapgouv/tchap-android-v2/blob/develop/CONTRIBUTING.md) if you want to contribute on the Tchap Android project! + +Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org). + +Also [this documentation](./docs/_developer_onboarding.md) can hopefully help developers to start working on the project. + +## Triaging issues + +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. + +>>>>>>> v1.5.2 diff --git a/TCHAP_CHANGES.md b/TCHAP_CHANGES.md index 43dbc6e3b5..144b668783 100644 --- a/TCHAP_CHANGES.md +++ b/TCHAP_CHANGES.md @@ -1,3 +1,17 @@ +Changes in Tchap 2.5.2 (2022-12-18) +=================================== + +Bugfixes 🐛 +---------- + - Crash on open the "access by link" screen ([#776](https://github.com/tchapgouv/tchap-android-v2/issues/776)) + +Other changes +------------- + - Rebase against Element-Android v1.5.2 ([#764](https://github.com/tchapgouv/tchap-android-v2/issues/764)) + - Activate the antivirus for audio files ([#780](https://github.com/tchapgouv/tchap-android-v2/issues/780)) + - Disable app layout by default ([#784](https://github.com/tchapgouv/tchap-android-v2/issues/784)) + + Changes in Tchap 2.5.0 (2022-11-10) =================================== diff --git a/build.gradle b/build.gradle index 4f9469f8fc..713d12d9af 100644 --- a/build.gradle +++ b/build.gradle @@ -25,14 +25,15 @@ buildscript { classpath libs.gradle.kotlinPlugin classpath libs.gradle.hiltPlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3' - classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.google.gms:google-services:4.3.14' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.2.2" - classpath 'org.owasp:dependency-check-gradle:7.1.2' + classpath 'org.owasp:dependency-check-gradle:7.2.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.10" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' + classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -46,6 +47,8 @@ plugins { // Dependency Analysis id 'com.autonomousapps.dependency-analysis' version "1.13.1" + // Gradle doctor + id "com.osacky.doctor" version "0.8.1" } // https://github.com/jeremylong/DependencyCheck @@ -58,6 +61,9 @@ dependencyCheck { ] } +// Gradle doctor configuration +apply from: './tools/gradle/doctor.gradle' + allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" apply plugin: "io.gitlab.arturbosch.detekt" @@ -71,6 +77,14 @@ allprojects { groups.mavenCentral.group.each { includeGroup it } } } + // snapshots repository + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + content { + groups.snapshot.regex.each { includeGroupByRegex it } + groups.snapshot.group.each { includeGroup it } + } + } maven { url 'https://jitpack.io' content { @@ -216,7 +230,7 @@ project(":vector") { } } -project(":library:diff-match-patch") { +project(":library:external:diff-match-patch") { sonarqube { skipProject = true } @@ -287,3 +301,30 @@ dependencyAnalysis { } } } + +tasks.register("recordScreenshots", GradleBuild) { + startParameter.projectProperties.screenshot = "" + tasks = [':vector:recordPaparazziDebug'] +} + +// Tchap : remove task not found +//tasks.register("verifyScreenshots", GradleBuild) { +// startParameter.projectProperties.screenshot = "" +// tasks = [':vector:verifyPaparazziDebug'] +//} + +ext.initScreenshotTests = { project -> + def hasScreenshots = project.hasProperty("screenshot") + if (hasScreenshots) { + project.apply plugin: 'app.cash.paparazzi' + } + project.dependencies { testCompileOnly "app.cash.paparazzi:paparazzi:1.0.0" } + project.android.testOptions.unitTests.all { + def screenshotTestCapture = "**/*ScreenshotTest*" + if (hasScreenshots) { + include screenshotTestCapture + } else { + exclude screenshotTestCapture + } + } +} diff --git a/coverage.gradle b/coverage.gradle index 7721543352..f6ce9db551 100644 --- a/coverage.gradle +++ b/coverage.gradle @@ -81,7 +81,8 @@ task generateCoverageReport(type: JacocoReport) { task unitTestsWithCoverage(type: GradleBuild) { // the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage startParameter.projectProperties.coverage = [enableTestCoverage: false] - tasks = [':vector:testGplayBtchapWithoutvoipWithoutpinningDebugUnitTest', ':matrix-sdk-android:testDebugUnitTest'] + // Tchap: Specify variant for every module + tasks = [':matrix-sdk-android:testDebugUnitTest', ':vector:testGplayBtchapWithoutvoipWithoutpinningDebugUnitTest', ':vector-app:testGplayBtchapWithoutvoipWithoutpinningDebugUnitTest', ':vector-config:testBtchapDebugUnitTest'] } task instrumentationTestsWithCoverage(type: GradleBuild) { diff --git a/dependencies.gradle b/dependencies.gradle index 3759763fb7..3bf3ab746d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,36 +1,37 @@ ext.versions = [ 'minSdk' : 21, - 'compileSdk' : 31, - 'targetSdk' : 31, + 'compileSdk' : 32, + 'targetSdk' : 32, 'sourceCompat' : JavaVersion.VERSION_11, 'targetCompat' : JavaVersion.VERSION_11, ] - -// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142 -// Please test carefully before upgrading again. -def gradle = "7.1.3" +def gradle = "7.2.2" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.6.21" +def kotlin = "1.7.20" def kotlinCoroutines = "1.6.4" -def dagger = "2.42" -def appDistribution = "16.0.0-beta03" +def dagger = "2.44" +def appDistribution = "16.0.0-beta04" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" -def moshi = "1.13.0" +def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.163.0" +def flipper = "0.164.0" def epoxy = "4.6.2" def mavericks = "2.7.0" -def glide = "4.13.2" +def glide = "4.14.1" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" -def vanniktechEmoji = "0.15.0" +// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert +// the whole commit which set version 0.16.0-SNAPSHOT +def vanniktechEmoji = "0.16.0-SNAPSHOT" + +def sentry = "6.4.1" -def fragment = "1.5.2" +def fragment = "1.5.3" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 @@ -51,7 +52,7 @@ ext.libs = [ ], androidx : [ 'activity' : "androidx.activity:activity:1.5.1", - 'appCompat' : "androidx.appcompat:appcompat:1.4.2", + 'appCompat' : "androidx.appcompat:appcompat:1.5.1", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.8.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", @@ -86,7 +87,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.54" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.56" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -100,7 +101,7 @@ ext.libs = [ 'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper", ], element : [ - 'opusencoder' : "io.element.android:opusencoder:1.0.4", + 'opusencoder' : "io.element.android:opusencoder:1.1.0", ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -120,6 +121,7 @@ ext.libs = [ markwon : [ 'core' : "io.noties.markwon:core:$markwon", 'extLatex' : "io.noties.markwon:ext-latex:$markwon", + 'imageGlide' : "io.noties.markwon:image-glide:$markwon", 'inlineParser' : "io.noties.markwon:inline-parser:$markwon", 'html' : "io.noties.markwon:html:$markwon" ], @@ -165,10 +167,13 @@ ext.libs = [ apache : [ 'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator" ], + sentry: [ + 'sentryAndroid' : "io.sentry:sentry-android:$sentry" + ], tests : [ 'kluent' : "org.amshove.kluent:kluent-android:1.68", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", - 'junit' : "junit:junit:4.13.2" + 'junit' : "junit:junit:4.13.2", ] ] diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index bcd737acc9..cdab6172d1 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -38,10 +38,18 @@ ext.groups = [ 'com.google.testing.platform', ] ], + snapshot: [ + regex: [ + ], + group: [ + 'com.vanniktech', + ] + ], mavenCentral: [ regex: [ ], group: [ + 'app.cash.paparazzi', 'ch.qos.logback', 'com.adevinta.android', 'com.airbnb.android', @@ -69,8 +77,6 @@ ext.groups = [ 'com.gabrielittner.threetenbp', 'com.getkeepsafe.relinker', 'com.github.bumptech.glide', - 'com.github.filippudak', - 'com.github.filippudak.progresspieview', 'com.github.javaparser', 'com.github.piasy', 'com.github.shyiko.klob', @@ -120,7 +126,7 @@ ext.groups = [ 'com.sun.xml.bind.mvn', 'com.sun.xml.fastinfoset', 'com.thoughtworks.qdox', - 'com.vanniktech', + // 'com.vanniktech', 'commons-cli', 'commons-codec', 'commons-io', @@ -142,14 +148,18 @@ ext.groups = [ 'io.opencensus', 'io.reactivex.rxjava2', 'io.realm', + 'io.sentry', 'it.unimi.dsi', 'jakarta.activation', 'jakarta.xml.bind', + 'javax.activation', 'javax.annotation', 'javax.inject', + 'javax.xml.bind', 'jline', 'jp.wasabeef', 'junit', + 'kxml2', 'me.saket', 'net.bytebuddy', 'net.java', @@ -178,11 +188,13 @@ ext.groups = [ 'org.hamcrest', 'org.jacoco', 'org.java-websocket', + 'org.jcodec', 'org.jetbrains', 'org.jetbrains.dokka', 'org.jetbrains.intellij.deps', 'org.jetbrains.kotlin', 'org.jetbrains.kotlinx', + 'org.jetbrains.trove4j', 'org.json', 'org.jsoup', 'org.junit', @@ -199,7 +211,6 @@ ext.groups = [ 'org.ow2.asm', 'org.ow2.asm', 'org.reactivestreams', - 'org.robolectric', 'org.slf4j', 'org.sonatype.oss', 'org.testng', diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md new file mode 100644 index 0000000000..2f414063e3 --- /dev/null +++ b/docs/_developer_onboarding.md @@ -0,0 +1,259 @@ +# Developer on boarding + + + +* [Introduction](#introduction) + * [Quick introduction to Matrix](#quick-introduction-to-matrix) + * [Matrix data](#matrix-data) + * [Room](#room) + * [Event](#event) + * [Sync](#sync) + * [Glossary about syncs](#glossary-about-syncs) + * [The Android project](#the-android-project) + * [Matrix SDK](#matrix-sdk) + * [Application](#application) + * [MvRx](#mvrx) + * [Behavior](#behavior) + * [Epoxy](#epoxy) + * [Other frameworks](#other-frameworks) + * [Push](#push) + * [Dependencies management](#dependencies-management) + * [Test](#test) + * [Other points](#other-points) + * [Logging](#logging) + * [Rageshake](#rageshake) + * [Tips](#tips) +* [Happy coding!](#happy-coding) + + + +## Introduction + +This doc is a quick introduction about the project and its architecture. + +It's aim is to help new developers to understand the overall project and where to start developing. + +Other useful documentation: +- all the docs in this folder! +- the [contributing doc](../CONTRIBUTING.md), that you should also read carefully. + +### Quick introduction to Matrix + +Matrix website: [matrix.org](https://matrix.org), [discover page](https://matrix.org/discover). +*Note*: Matrix.org is also hosting a homeserver ([.well-known file](https://matrix.org/.well-known/matrix/client)). +The reference homeserver (this is how Matrix servers are called) implementation is [Synapse](https://github.com/matrix-org/synapse/). But other implementations exist. The Matrix specification is here to ensure that any Matrix client, such as Element Android and its SDK can talk to any Matrix server. + +Have a quick look to the client-server API documentation: [Client-server documentation](https://spec.matrix.org/v1.3/client-server-api/). Other network API exist, the list is here: (https://spec.matrix.org/latest/) + +Matrix is an open source protocol. Change are possible and are tracked using [this GitHub repository](https://github.com/matrix-org/matrix-doc/). Changes to the protocol are called MSC: Matrix Spec Change. These are PullRequest to this project. + +Matrix object are Json data. Unstable prefixes must be used for Json keys when the MSC is not merged (i.e. accepted). + +#### Matrix data + +There are many object and data in the Matrix worlds. Let's focus on the most important and used, `Room` and `Event` + +##### Room + +`Room` is a place which contains ordered `Event`s. They are identified with their `room_id`. Nearly all the data are stored in rooms, and shared using homeserver to all the Room Member. + +*Note*: Spaces are also Rooms with a different `type`. + +##### Event + +`Events` are items of a Room, where data is embedded. + +There are 2 types of Room Event: + +- Regular Events: contain useful content for the user (message, image, etc.), but are not necessarily displayed as this in the timeline (reaction, message edition, call signaling). +- State Events: contain the state of the Room (name, topic, etc.). They have a non null value for the key `state_key`. + +Also all the Room Member details are in State Events: one State Event per member. In this case, the `state_key` is the matrixId (= userId). + +Important Fields of an Event: +- `event_id`: unique across the Matrix universe; +- `room_id`: the room the Event belongs to; +- `type`: describe what the Event contain, especially in the `content` section, and how the SDK should handle this Event; +- `content`: dynamic Event data; depends on the `type`. + +So we have a triple `event_id`, `type`, `state_key` which uniquely defines an Event. + +#### Sync + +The `Sync` is a way for the Matrix client to be up to date regarding the user data hosted by the server. All the Events are coming through the sync response. More details can be found here: [spec.matrix.org/v1.3/client-server-api/#syncing](https://spec.matrix.org/v1.3/client-server-api/#syncing) +When the application is in foreground, this is a looping request. We are using Https requests, which offer the advantage to be compatible with any homeserver. A sync token is used as request parameter, to let the server know what the client knows. +The `SyncThread` is responsible to manage the sync request loop. + +When the application is in background, a Push will trigger a sync request. + +##### Glossary about syncs + +- **initial sync**: a sync request without a token. This is the first request a client perform after login or after a clear cache. The server will include in the response all your rooms with the full state (all the room membership Event will not be present), with the latest messages for each room. We are in the process to replace this by version 3: sliding sync. All data are inserted to the Database (currently [Realm](https://www.mongodb.com/docs/realm/sdk/java/)). +- **incremental sync**: sync request with a token. +- **gappy sync**: sync request where all the new Events are not returned for one or several Rooms. Also called `limited sync`. It can be limited per Room. To get all the missing Events, a Room pagination API has to be called. +- **sync token**: `next_batch` value in the previous sync response. Will be provided as the `since` parameter for the next sync request. + +### The Android project + +The project should compile out of the box. + +The project is split into several modules. The main ones are: +For the app +- `vector-app`: application entry point; +- `vector`: legacy application, but now a library. In the process of being split into several modules; +- `vector-config`: this is where all the configuration of the application should occurs. Should because we are in the process of migrating all the configuration here; +- `library/ui-strings`: this is where all the string resources are stored. Please refer to [contributing doc](../CONTRIBUTING.md) to know how to make change on this module; +- `library/ui-styles`: this is where the Android styles are defined. + +For the SDK +- `matrix-sdk-android`: the main SDK module. The sources are in this project, but are also exported to [its own project](https://github.com/matrix-org/matrix-android-sdk2). All the PRs and issues related to the SDK take place in the Element Android project; +- `matrix-sdk-android-flow`: contains some wrapper to expose `Flow` to the application. + +### Matrix SDK + +SDK exposes `Services` to the client application. `Services` are public interface, and are defined in this parent package: `org.matrix.android.sdk.api`. Default implementation are internal to the SDK, in this parent package: `org.matrix.android.sdk.internal`. Note that you also have to declare the classes as `internal` when adding classes to the `org.matrix.android.sdk.internal` package. + +Interface allows us to replace the implementation for testing purpose. + +A generated documentation of the SDK is available [here](https://matrix-org.github.io/matrix-android-sdk2/). Updated after each release. Please ensure that the documentation (KDoc) of all the SDK Services is up to date, and is clear for a SDK user. +The SDK generated documentation also contains information about the entry points of the SDK. + +[Dagger](https://dagger.dev/) is used to inject all the dependencies to the SDK classes. + +SDK is exposing data as `LiveData`, but we are progressively migrating to `Flow`. Database is the source of truth. + +Example: +- Client send an Event using the `SendService`; +- At the end a `SendEvent` task is used; +- Retrofit API is used to send data to the server; +- Goes to the server, which returns only the `event_id`; +- The `Event` is coming back from the `sync` response with eventually extra added data. + +### Application + +This is the UI part of the project. + +There are two variants of the application: `Gplay` and `Fdroid`. + +The main difference is about using Firebase on `Gplay` variant, to have Push from Google Services. `FDroid` variant cannot contain closed source dependency. + +`Fdroid` is using background polling to lack the missing of Pushed. Now a solution using UnifiedPush has ben added to the project. See refer to [the dedicated documentation](./unifiedpush.md) for more details. + +#### MvRx + +[Maverick](https://airbnb.io/mavericks/#/README) (or MvRx) is an Android MVI framework that helps to develop Reactive application on Android. + +- Activity: holder for Fragment. See the parent [VectorBaseActivity](../vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt); +- Fragment: manage screen of the application. See the parent [VectorBaseFragment](../vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt); +- BottomSheet: see the parent [VectorBaseBottomSheetDialogFragment](../vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt); +- ViewModel: this is where the logic is placed. All our ViewModel has a `handle()` which takes action as parameter. See the parent [VectorViewModel](../vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt); +- VectorSharedActionViewModel: Specific ViewModel that can be used to communicate between Fragment(s) and the host Activity. See the parent [VectorSharedActionViewModel](../vector/src/main/java/im/vector/app/core/platform/VectorSharedActionViewModel.kt); +- ViewState: this are `data class`, and this represent the state of the View. Has to be copied and set to be updated. Fragment will update the UI regarding the current state (`invalidate()` method). `Async` class from MvRx can be used in the ViewState, especially for asynchronous data loading. Nullability can also be used for optional data. ViewStates have to implement `MavericksState`; +- ViewEvents: useful when the ViewModel asks the View to trigger a specific action: navigation, show dialog, etc. See the parent [VectorViewEvents](../vector/src/main/java/im/vector/app/core/platform/VectorViewEvents.kt); +- ViewAction (`VectorViewModelAction`): useful when the UI (generally the Fragment) asks the ViewModel to do something. See the parent [VectorViewModelAction](../vector/src/main/java/im/vector/app/core/platform/VectorViewModelAction.kt); +- Controller: see the `Epoxy` section just below. + +##### Behavior + +Fragment asks the ViewModel to perform an action (coming from the user, but not necessarily. ViewModel can then talk to the SDK, updates the state once or several times. Fragment update the UI regarding the new state. + +When ViewModel is instantiated, it can subscribe using the SDK Services to get live state of the data. + +`invalidate()` has to be used by default, but it's possible to listen to specific member(s) of the `ViewState` using `onEach`. TODO Add an example. +`awaitState()` method + +#### Epoxy + +[Epoxy](https://github.com/airbnb/epoxy) is an Android library for building complex screens in a RecyclerView. Please read [the introduction](https://github.com/airbnb/epoxy#epoxy). + +- Controller declares items of the RecyclerView. Controller is injected in the Fragment. Controller extends `EpoxyController`, or one of its subclass, especially `TypedEpoxyController`; +- Fragment gives the state to the controller using `setData`; +- `buildModels` will be called by the framework; +- Controller will create ordered Items. + +Epoxy does the diffing, and handle many other thing for us, like handling item type, etc. + +See for instance the controller [AccountDataEpoxyController](../vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt)) for a simple example. + +Warning: do not use twice the same item `id` or it will crash. + +#### Other frameworks + +- Dependency injection is managed by [Dagger](https://dagger.dev/) (SDK) and [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) (App); +- [Retrofit](https://square.github.io/retrofit/) and [OkHttp3](https://square.github.io/okhttp/): network requests; +- [Moshi](https://github.com/square/moshi) is used to parse and serialize Json object; + +### Push + +Please see the dedicated documentation for more details. + +This is the classical scenario: + +- App receives a Push. Note: Push is ignored if app is in foreground; +- App asks the SDK to load Event data (fastlane mode). We have a change to get the data faster and display the notification faster; +- App asks the SDK to perform a sync request. + +### Dependencies management + +All the dependencies are declared in `build.gradle` files. But some versions are declared in [this dedicated file](../dependencies.gradle). + +When adding a new dependency, you will have to update the file [dependencies_groups.gradle](../dependencies_groups.gradle) to allow the dependency to be downloaded from the artifact repository. Sometimes sub-dependencies need to be added too, until the project can compile. + +[Dependabot](https://github.com/dependabot) is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one. +dependencies_group, gradle files, Dependabot, etc. + +### Test + +Please refer to [this dedicated document](./ui-tests.md). + +TODO add link to the dedicated screenshot test documentation + +### Other points + +#### Logging + +**Important warning: ** NEVER log private user data, or use the flag `LOG_PRIVATE_DATA`. Be very careful when logging `data class`, all the content will be output! + +[Timber](https://github.com/JakeWharton/timber) is used to log data to logcat. We do not use directly the `Log` class. If possible please use a tag, as per + +````kotlin +Timber.tag(loggerTag.value).d("my log") +```` + +because automatic tag (= class name) will not be available on the release version. + +Also generally it is recommended to provide the `Throwable` to the Timber log functions. + +Last point, not that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up. + +#### Rageshake + +Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report. + +Bug report can contain: +- a screenshot of the current application state +- the application logs from up to 15 application starts +- the logcat logs +- the key share history (crypto data) + +The data will be sent to an internal server, which is not publicly accessible. A GitHub issue will also be created to a private GitHub repository. + +Rageshake can be very useful to get logs from a release version of the application. + +### Tips + +- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here; +- Show hidden Events can also help to debug feature. When developer mode is enabled, it is possible to view the source (= the Json content) of any Events; +- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; +- Hidden debug menu: when developer mode is enabled and on debug build, there are some extra screens that can be accessible using the green wheel. In those screens, it will be possible to toggle some feature flags; +- Using logcat, filtering with `onResume` can help you to understand what screen are currently displayed on your device. Searching for string displayed on the screen can also help to find the running code in the codebase. +- When this is possible, prefer using `sealed interface` instead of `sealed class`; +- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI will detect this String and will warn the user about it. + +## Happy coding! + +The team is here to support you, feel free to ask anything to other developers. + +Also please feel to update this documentation, if incomplete/wrong/obsolete/etc. + +**Thanks!** diff --git a/docs/danger.md b/docs/danger.md index afa3555469..34baa62e9e 100644 --- a/docs/danger.md +++ b/docs/danger.md @@ -28,6 +28,7 @@ Here are the checks that Danger does so far: - PR with change on layout should include screenshot in the description - PR which adds png file warn about the usage of vector drawables - non draft PR should have a reviewer +- files containing translations are not modified by developers ### Quality check diff --git a/docs/screenshot_testing.md b/docs/screenshot_testing.md new file mode 100644 index 0000000000..93b91cdf67 --- /dev/null +++ b/docs/screenshot_testing.md @@ -0,0 +1,72 @@ +# Screenshot testing + + + +* [Overview](#overview) +* [Setup](#setup) +* [Recording](#recording) +* [Verifying](#verifying) +* [Contributing](#contributing) +* [Example](#example) + + + +## Overview + +- Screenshot tests are tests which record the content of a rendered screen and verify subsequent runs to check if the screen renders differently. +- Element uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify android layouts. +- The screenshot verification occurs on every pull request as part of the `tests.yml` workflow. + +## Setup + +- Install Git LFS through your package manager of choice (`brew install git-lfs` | `yay -S git-lfs`). +- Install the Git LFS hooks into the project. + +```bash +# with element-android as the current working directory +git lfs install --local +``` + +- If installed correctly, `git push` and `git pull` will now include LFS content. + +## Recording + +- `./gradlew recordScreenshots` +- Paparazzi will generate images in `${module}/src/test/snapshots`, which will need to be committed to the repository using Git LFS. + +## Verifying + +- `./gradlew verifyScreenshots` +- In the case of failure, Paparazzi will generate images in `${module}/out/failure`. The images will show the expected and actual screenshots along with a delta of the two images. + +## Contributing + +- When creating a test, the file (and class) name names must include `ScreenshotTest`, eg `ItemScreenshotTest`. +- After creating the new test, record and commit the newly rendered screens. +- `./tools/validate_lfs` can be ran to ensure everything is working correctly with Git LFS, the CI also runs this check. + +## Example + +```kotlin +class PaparazziExampleScreenshotTest { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = PIXEL_3, + theme = "Theme.Vector.Light", + ) + + @Test + fun `example paparazzi test`() { + // Inflate the layout + val view = paparazzi.inflate(R.layout.item_radio) + + // Bind data to the view + view.findViewById(R.id.actionTitle).text = paparazzi.resources.getString(R.string.room_settings_all_messages) + view.findViewById(R.id.radioIcon).setImageResource(R.drawable.ic_radio_on) + + // Record the bound view + paparazzi.snapshot(view) + } +} +``` diff --git a/docs/unit_testing.md b/docs/unit_testing.md new file mode 100644 index 0000000000..f786c9a160 --- /dev/null +++ b/docs/unit_testing.md @@ -0,0 +1,351 @@ +# Table of Contents + + + +* [Overview](#overview) + * [Best Practices](#best-practices) +* [Project Conventions](#project-conventions) + * [Setup](#setup) + * [Naming](#naming) + * [Format](#format) + * [Assertions](#assertions) + * [Constants](#constants) + * [Mocking](#mocking) + * [Fakes](#fakes) + * [Fixtures](#fixtures) + * [Examples](#examples) + * [Extensions used to streamline the test setup](#extensions-used-to-streamline-the-test-setup) + * [Fakes and Fixtures](#fakes-and-fixtures) + + + +## Overview + +Unit tests are a mechanism to validate our code executes the way we expect. They help to inform the design of our systems by requiring testability and +understanding, they describe the inner workings without relying on inline comments and protect from unexpected regressions. + +However, unit tests are not a magical solution to solve all our problems and come at a cost. Unreliable and hard to maintain tests often end up ignored, deleted +or worse, provide a false sense of security. + +### Best Practices + +Tests can be written in many ways, the main rule is to keep them simple and maintainable. Some ways to help achieve this are... + +- Break out logic into single units (following the Single Responsibility Principle) to reduce test complexity. +- Favour pure functions, avoiding mutable state. +- Prefer dependency injection to static calls to allow for simpler test setup. +- Write concise tests with a single function under test, clearly showing the inputs and expected output. +- Create separate test cases instead of changing parameters and grouping multiple assertions within a single test to help trace back failure causes (with the + exception of parameterised tests). +- Assert against entire models instead of subsets of properties to capture any possible changes within the test scope. +- Avoid invoking logic from production instances other than the class under test to guard from unrelated changes. +- Always inject `Dispatchers` and `Clock` instances and provide fake implementations for tests to avoid non deterministic results. + +## Project Conventions + +#### Setup + +- Test file and class name should be the class under test with the Test suffix, created in a `test` sourceset, with the same package name as the class under + test. +- Dependencies of the class are instantiated inline, junit will recreate the test class for each test run. +- A line break between the dependencies and class under test helps clarify the instance being tested. + +```kotlin + +class MyClassTest { + + private val fakeUppercaser = FakeUppercaser() + + // line break between the class under test and its dependencies + private val myClass = MyClass(fakeUppercaser.instance) +} + +``` + +#### Naming + +- Test names use the `Gherkin` format, `given, when, then` mapping to the input, logic under test and expected result. + - `given` - Uniqueness about the environment or dependencies in which the test case is running. _"given device is android 12 and supports dark mode"_ + - `when` - The action/function under test. _"when reading dark mode status"_ + - `then` - The expected result from the combination of _given_ and _when_. _"then returns dark mode enabled"_ +- Test names are written using kotlin back ticks to enable sentences _ish_. + +```kotlin +@Test +fun `given a lowercase label, when uppercasing, then returns label uppercased` +``` + +When the input is given directly to the _when_, this can also be represented as... + +```kotlin +@Test +fun `when uppercasing a lowercase label, then returns label uppercased` +``` + +Multiple given or returns statements can be used in the name although it could be a sign that the logic being tested does too much. + +--- + +#### Format + +- Test bodies are broken into sections through the use of blank lines where the sections correspond to the test name. +- Sections can span multiple lines. + +```kotlin +// comments are for illustrative purposes +/* given */ val lowercaseLabel = "hello world" + +/* when */ val result = textUppercaser.uppercase(lowercaseLabel) + +/* then */ result shouldBeEqualTo "HELLO WORLD" +``` + +- Functions extracted from test bodies are placed beneath all the unit tests. + +--- + +#### Assertions + +- Assertions against test results are made using [Kluent's](https://github.com/MarkusAmshove/Kluent) _fluent_ api. +- Typically `shouldBeEqualTo`is the main assertion to use for asserting function return values as by project convention we assert against entire objects or + lists. + +```kotlin +val result = listOf("hello", "world") + +// Fail +result shouldBeEqualTo listOf("hello") +``` + +```kotlin +data class Person(val age: Int, val name: String) + +val result = Person(age = 100, name = "Gandalf") + +// Avoid +result.age shouldBeEqualTo 100 + +// Prefer +result shouldBeEqualTo Person(age = 100, "Gandalf") +``` + +- Exception throwing can be asserted against using `assertFailsWith`. +- When asserting reusable exceptions, include the message to distinguish between them. + +```kotlin +assertFailsWith(message = "Details about error") { + // when section of the test + codeUnderTest() +} +``` + +--- + +#### Constants + +- Reusable values are extracted to file level immutable properties or constants. +- These can be parameters or expected results. +- The naming convention is to prefix with `A` or `AN` for better matching with the test name. + +```kotlin +private const val A_LOWERCASE_LABEL = "hello" + +class MyTest { + @Test + fun `when uppercasing a lowercase label, then returns label uppercased`() { + val result = TextUppercaser().uppercase(A_LOWERCASE_LABEL) + ... + } +} +``` + +--- + +#### Mocking + +- In order to provide different behaviour for dependencies within tests our main method is through mocking, using [Mockk](https://mockk.io/). +- We avoid using relaxed mocks in favour of explicitly declaring mock behaviour through the _Fake_ convention. There are exceptions when mocking framework + classes which would require a lot of boilerplate. +- Using `Spy` is discouraged as it inherently requires real instances, which we are avoiding in our tests. There are exceptions such as `VectorFeatures` which + acts like a `Fixture` in release builds. + +--- + +#### Fakes + +- Fakes are reusable instances of classes purely for testing purposes. They provide functions to replace the functions of the interface/class they're faking + with test specific values. +- When faking an interface, the _Fake_ can be written using delegation or by stubbing +- All Fakes currently reside in the same package `${package}.test.fakes` + +```kotlin +// Delegating to a mock +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +} + +// Stubbing the interface +class FakeClock(private val epoch: Long) : Clock { + override fun epochMillis() = epoch +} +``` + +It's currently more common for fakes to fake class behaviour, we achieve this by wrapping and exposing a mock instance. + +```kotlin +class FakeCursor { + val instance = mockk() + fun givenEmpty() { + every { instance.count } returns 0 + every { instance.moveToFirst() } returns false + } +} + +val fakeCursor = FakeCursor().apply { givenEmpty() } +``` + +#### Fixtures + +- Fixtures are a reusable wrappers around data models. They provide default values to make creating instances as easy as possible, with the option to override + specific parameters when needed. +- Are namespaced within an `object`. +- Reduces the _find usages_ noise when searching for usages of the origin class construction. +- All Fixtures currently reside in the same package `${package}.test.fixtures`. + +```kotlin +object ContentAttachmentDataFixture { + fun aContentAttachmentData( + type: ContentAttachmentData.Type.TEXT, + mimeType: String? = null + ) = ContentAttachmentData(type, mimeType) +} +``` + +- Fixtures can also be used to manage specific combinations of parameters + +```kotlin +fun aContentAttachmentAudioData() = aContentAttachmentData( + type = ContentAttachmentData.Type.AUDIO, + mimeType = "audio/mp3", +) +``` + +--- + +### Examples + +##### Extensions used to streamline the test setup + +```kotlin +class CircularCacheTest { + + @Test + fun `when putting more than cache size then cache is limited to cache size`() { + val (cache, internalData) = createIntCache(cacheSize = 3) + + cache.putInOrder(1, 1, 1, 1, 1, 1) + + internalData shouldBeEqualTo arrayOf(1, 1, 1) + } +} + +private fun createIntCache(cacheSize: Int): Pair, Array> { + var internalData: Array? = null + val factory: (Int) -> Array = { + Array(it) { null }.also { array -> internalData = array } + } + return CircularCache(cacheSize, factory) to internalData!! +} + +private fun CircularCache.putInOrder(vararg values: Int) { + values.forEach { put(it) } +} +``` + +##### Fakes and Fixtures + +```kotlin +class LateInitUserPropertiesFactoryTest { + + private val fakeActiveSessionDataSource = FakeActiveSessionDataSource() + private val fakeVectorStore = FakeVectorStore() + private val fakeContext = FakeContext() + private val fakeSession = FakeSession().also { + it.givenVectorStore(fakeVectorStore.instance) + } + + private val lateInitUserProperties = LateInitUserPropertiesFactory( + fakeActiveSessionDataSource.instance, + fakeContext.instance + ) + + @Test + fun `given no active session, when creating properties, then returns null`() { + val result = lateInitUserProperties.createUserProperties() + + result shouldBeEqualTo null + } + + @Test + fun `given a teams use case set on an active session, when creating properties, then includes the remapped WorkMessaging selection`() { + fakeVectorStore.givenUseCase(FtueUseCase.TEAMS) + fakeActiveSessionDataSource.setActiveSession(fakeSession) + + val result = lateInitUserProperties.createUserProperties() + + result shouldBeEqualTo UserProperties( + ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging + ) + } +} + ``` + +##### ViewModel + +- `ViewModels` tend to be one of the most complex areas to unit test due to their position as a coordinator of data flows and bridge between domains. +- As the project uses a slightly tweaked`MvRx`, our API for the `ViewModel` is simplified down to `input - ViewModel.handle(Action)` + and `output Flows - ViewModel.viewEvents & ViewModel.stateFlow`. A `ViewModel` test asserter has been created to further simplify the process. + +```kotlin +class ViewModelTest { + + private var initialState = ViewState.Empty + + @get:Rule + val mvrxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher()) + + @Test + fun `when handling MyAction, then emits Loading and Content states`() { + val viewModel = ViewModel(initialState) + val test = viewModel.test() // must be invoked before interacting with the VM + + viewModel.handle(MyAction) + + test + .assertViewStates(initialState, State.Loading, State.Content()) + .assertNoEvents() + .finish() + } +} +``` + +- `ViewModels` often emit multiple states which are copies of the previous state, the `test` extension `assertStatesChanges` allows only the difference to be + supplied. + +```kotlin +data class ViewState(val name: String? = null, val age: Int? = null) +val initialState = ViewState() +val viewModel = ViewModel(initialState) +val test = viewModel.test() + +viewModel.handle(ChangeNameAction("Gandalf")) + +test + .assertStatesChanges( + initialState, + { copy(name = "Gandalf") }, + ) + .finish() +``` diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104360.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104360.txt new file mode 100644 index 0000000000..fcadf9898c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Nový vzhled aplikace lze povolit v Experimentálních funkcích. Prosíme, vyzkoušejte ho! +Oprava problémů s chybějícími oznámeními a dlouhou přírůstkovou synchronizací. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104360.txt b/fastlane/metadata/android/de-DE/changelogs/40104360.txt new file mode 100644 index 0000000000..3c47fa7eb6 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Das neue App-Layout kann in den experimentellen Einstellungen aktiviert werden. Probier es gerne aus! +Fehler bzgl. ausbleibender Benachrichtigungen und langwierigem inkrementellem Synchronisieren behoben. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index d27bd3ef12..de571645ee 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Gruppen-Messenger - verschlüsselte Kommunikation, Gruppenchat und Videoanrufe +Gruppen-Messenger – verschlüsselte Kommunikation, Gruppen und Videoanrufe diff --git a/fastlane/metadata/android/de-DE/title.txt b/fastlane/metadata/android/de-DE/title.txt index 6304f37925..edee751d06 100644 --- a/fastlane/metadata/android/de-DE/title.txt +++ b/fastlane/metadata/android/de-DE/title.txt @@ -1 +1 @@ -Element - Sicherer Messenger +Element – Sicher kommunizieren diff --git a/fastlane/metadata/android/en-US/changelogs/40105000.txt b/fastlane/metadata/android/en-US/changelogs/40105000.txt new file mode 100644 index 0000000000..e86519e6e9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105000.txt @@ -0,0 +1,2 @@ +Main changes in this version: Deferred DM enabled by default. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105020.txt b/fastlane/metadata/android/en-US/changelogs/40105020.txt new file mode 100644 index 0000000000..41795c468c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105020.txt @@ -0,0 +1,2 @@ +Main changes in this version: New app layout enabled by default! +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104360.txt b/fastlane/metadata/android/et/changelogs/40104360.txt new file mode 100644 index 0000000000..1c2733683d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Testide alt saad sisse lülitada uue kujunduse - palun proovi seda! +Parandasime teavitustega seotud vigu ning andmete sünkroniseerimist pika viitega. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104360.txt b/fastlane/metadata/android/fa/changelogs/40104360.txt new file mode 100644 index 0000000000..be14e1b9e2 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104360.txt @@ -0,0 +1,3 @@ +چینش کارهٔ جدید می‌تواند در تنظیمات آزمایشگاه‌ها به کار بیفتند. لطفاً بیازماییدش! +رفع مشکلات مربوط به آگاهی غایب و همگام‌سازی تجمعّی طولانی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104360.txt b/fastlane/metadata/android/fr-FR/changelogs/40104360.txt new file mode 100644 index 0000000000..80f59952d1 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104360.txt @@ -0,0 +1,3 @@ +La nouvelle présentation de l’application est disponibles dans les paramètres expérimentaux. Essayez-là ! +Correction de problèmes sur les notifications manquantes, et la synchronisation incrémentale lente. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40104360.txt b/fastlane/metadata/android/hu-HU/changelogs/40104360.txt new file mode 100644 index 0000000000..a63a8d1a83 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Az új alkalmazás megjelenés a Laborokban bekapcsolható. Próbáld ki! +Hiányzó értesítések és hosszú inkrementális szinkronizáció javítása. +Teljes változásnapló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104360.txt b/fastlane/metadata/android/id/changelogs/40104360.txt new file mode 100644 index 0000000000..be626f6350 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Tata Letak Aplikasi Baru dapat diaktifkan di pengaturan Uji Coba. Cobalah! +Perbariki masalah tentang notifikasi hilang, dan penyinkronan inkremental panjang. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104360.txt b/fastlane/metadata/android/it-IT/changelogs/40104360.txt new file mode 100644 index 0000000000..c6749d3ff7 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Nuova disposizione dell'app attivabile nelle impostazioni Laboratori. Provala! +Corretti problemi su notifiche mancanti e sincronizzazioni incrementali lunghe. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lt/full_description.txt b/fastlane/metadata/android/lt/full_description.txt new file mode 100644 index 0000000000..7189008456 --- /dev/null +++ b/fastlane/metadata/android/lt/full_description.txt @@ -0,0 +1,42 @@ +Element yra ir saugaus žinučių siuntimo, ir produktyvaus komandinio bendradarbiavimo programėlė, puikiai tinkanti grupiniams pokalbiams dirbant nuotoliniu būdu. Ši pokalbių programa naudoja visapusį šifravimą, kad užtikrintų galingas vaizdo konferencijas, dalijimąsi failais ir balso skambučius. + +Element funkcijos turi: +- Išplėstinės bendravimo internetu priemonės +- Visiškai užšifruotos žinutės, kad būtų galima saugiau bendrauti su įmone, net ir su nuotoliniais darbuotojais +- Decentralizuoti pokalbiai, pagrįsti atvirojo kodo sistema Matrix +- Saugus dalijimasis failais su šifruotais duomenimis valdant projektus +- Vaizdo pokalbiai su IP balso perdavimu ir ekrano bendrinimu +- Lengva integracija su mėgstamiausiomis internetinėmis bendradarbiavimo priemonėmis, projektų valdymo įrankiais, VoIP paslaugomis ir kitomis komandinių pokalbių programomis + +Element visiškai skiriasi nuo kitų žinučių siuntimo ir bendradarbiavimo programėlių. Ji veikia Matrix - atvirame tinkle, skirtame saugiam žinučių siuntimui ir decentralizuotam bendravimui. Jame galima savarankiškai talpinti duomenis ir žinutes savo serveryje, kad naudotojai galėtų maksimaliai valdyti ir kontroliuoti savo duomenis ir žinutes. + +Privatumas ir šifruotos žinutės +Element apsaugo jus nuo nepageidaujamų reklamų, duomenų gavybos ir uždarų sodų. Jis taip pat apsaugo visus jūsų duomenis, "vienas su vienu" vaizdo ir balso ryšį, naudodamas visapusį šifravimą ir kryžmiškai pasirašytą įrenginių patvirtinimą. + +Element suteikia galimybę kontroliuoti savo privatumą ir kartu saugiai bendrauti su visais, esančiais Matrix tinkle, arba kitais verslo bendradarbiavimo įrankiais integruojantis su tokiomis programėlėmis kaip Slack. + +Element gali būti savarankiškai talpinamas +Kad galėtumėte geriau kontroliuoti savo slaptus duomenis ir pokalbius, Element gali būti savarankiškai talpinamas arba galite pasirinkti bet kurį Matrix pagrindu veikiantį serverį - atvirojo kodo decentralizuoto bendravimo standartu. Element suteikia privatumą, saugumo atititikimą ir integracijos lankstumą. + +Jūsų duomenys priklauso jums +Jūs nusprendžiate, kur laikyti savo duomenis ir žinutes. Be duomenų gavybos ar trečiųjų šalių prieigos rizikos. + +Element suteikia jums kontrolę įvairiais būdais: +1. Gaukite nemokamą paskyrą viešajame serveryje matrix.org, kurį talpina Matrix kūrėjai, arba rinkitės iš tūkstančių viešųjų serverių, kurių talpinimą teikia savanoriai +2. Savarankiškai talpinkite savo paskyrą, naudodami serverį savo IT infrastruktūroje +3. Užsisakykite paskyrą nuosavame serveryje tiesiog užsisakydami "Element Matrix Services" talpinimo paslaugą + +Atviras žinučių siuntimas ir bendradarbiavimas +Galite bendrauti su bet kuriuo Matrix tinklo nariu, nesvarbu, ar jis naudojasi Element, kita Matrix programėle, ar net jei naudoja kitą žinučių siuntimo programėlę. + +Super saugus +Tikras visapusis šifravimas (žinutes gali iššifruoti tik pokalbio dalyviai) ir kryžminiu parašu patvirtintas įrenginių patvirtinimas. + +Pilnas bendravimas ir integracija +Žinučių siuntimas, balso ir vaizdo skambučiai, failų ir ekrano bendrinimas ir daugybė integracijų, robotų ir valdiklių. Kurkite kambarius, bendruomenes, palaikykite ryšį ir atlikite darbus. + +Tęskite darbą ten, kur baigėte +Palaikykite ryšį, kad ir kur būtumėte, naudodami visiškai sinchronizuotą žinučių istoriją visuose įrenginiuose ir internete adresu https://app.element.io + +Atviras kodas +Element Android yra atvirojo kodo projektas, kurį talpina GitHub. Praneškite apie klaidas ir (arba) prisidėkite prie jo kūrimo adresu https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/lt/short_description.txt b/fastlane/metadata/android/lt/short_description.txt new file mode 100644 index 0000000000..600e76b35d --- /dev/null +++ b/fastlane/metadata/android/lt/short_description.txt @@ -0,0 +1 @@ +Grupiniai pokalbiai - šifruotos žinutės ir vaizdo skambučiai diff --git a/fastlane/metadata/android/lt/title.txt b/fastlane/metadata/android/lt/title.txt new file mode 100644 index 0000000000..d911c34bb2 --- /dev/null +++ b/fastlane/metadata/android/lt/title.txt @@ -0,0 +1 @@ +Element - Saugūs pokalbiai diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104180.txt b/fastlane/metadata/android/nl-NL/changelogs/40104180.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104190.txt b/fastlane/metadata/android/nl-NL/changelogs/40104190.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104200.txt b/fastlane/metadata/android/nl-NL/changelogs/40104200.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104220.txt b/fastlane/metadata/android/nl-NL/changelogs/40104220.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104230.txt b/fastlane/metadata/android/nl-NL/changelogs/40104230.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104240.txt b/fastlane/metadata/android/nl-NL/changelogs/40104240.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104250.txt b/fastlane/metadata/android/nl-NL/changelogs/40104250.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104270.txt b/fastlane/metadata/android/nl-NL/changelogs/40104270.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104280.txt b/fastlane/metadata/android/nl-NL/changelogs/40104280.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104280.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104320.txt b/fastlane/metadata/android/nl-NL/changelogs/40104320.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104320.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/nl-NL/changelogs/40104340.txt b/fastlane/metadata/android/nl-NL/changelogs/40104340.txt new file mode 100644 index 0000000000..48796d85bc --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/40104340.txt @@ -0,0 +1,2 @@ +Belangrijkste veranderingen in deze versie: Verscheidene foutoplossingen en stabiliteitsverbeteringen. +Volledige lijst met veranderingen: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104360.txt b/fastlane/metadata/android/pt-BR/changelogs/40104360.txt new file mode 100644 index 0000000000..78a879ccb7 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Novo Layout de App poder ser habilitado nas configurações de Labs. Por favor dê uma chance! +Consertar problemas sobre notificação faltando, e sinc incremental longo. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104360.txt b/fastlane/metadata/android/sk/changelogs/40104360.txt new file mode 100644 index 0000000000..af4154b5cf --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Nové usporiadanie aplikácie môžete povoliť v nastaveniach laboratórií. Vyskúšajte to! +Oprava problémov týkajúcich sa chýbajúcich oznámení a dlhej inkrementálnej synchronizácie. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104360.txt b/fastlane/metadata/android/uk/changelogs/40104360.txt new file mode 100644 index 0000000000..a2c9bcc4b5 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Новий макет програми можна увімкнути в налаштуваннях лабораторії. Спробуйте! +Виправлено проблеми з відсутністю сповіщень та тривалою інкрементною синхронізацією. +Список усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index c046d8a40a..330ddde4ae 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -5,7 +5,7 @@ Element — це і безпечний месенджер, і застосуно - Повністю зашифровані повідомлення для надання можливості безпечнішого корпоративного спілкування, навіть для віддалених працівників - Децентралізований чат на основі відкритого коду Matrix - Безпечний обмін файлами із зашифрованими даними для керування проєктами -- Відеочати з передачею голосу через IP та показом екрану іншим +- Відеочати з передачею голосу через IP та показом екрана іншим - Проста інтеграція з вашими улюбленими інструментами для онлайн-співпраці, інструментами керування проєктами, послугами VoIP та іншими застосунками обміну повідомленнями для команд Element цілковито відрізняється від інших застосунків обміну повідомленнями та спільної роботи. Він працює на Matrix, відкритій мережі для безпечного обміну повідомленнями та децентралізованого зв'язку. Це дозволяє самостійне розгортання, щоб надати користувачам якнайбільше володіння та контролю над їх даними та повідомленнями. diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index 9b60098c34..03fdb6e34d 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -30,7 +30,7 @@ Element 透过不同的方式让你掌控一切: 你可以与 Matrix 网络上的任何人聊天,不论他们是使用 Element、其他 Matrix 应用或其他通讯应用。 超级安全 -真正的端到端加密(仅有那些在对话中的可以解密讯息)以及交叉签章装置验证。 +真正的端到端加密(仅有那些在对话中的人可以解密讯息)以及交叉签章装置验证。 完整的通讯与整合 信息传递、语音与视频通话、文件分享、画面分享与超多的整合、机器人与挂件。建构房间、社群、保持联络并完成工作。 diff --git a/fastlane/metadata/android/zh-CN/short_description.txt b/fastlane/metadata/android/zh-CN/short_description.txt index e271e7f9a4..8cfea85b90 100644 --- a/fastlane/metadata/android/zh-CN/short_description.txt +++ b/fastlane/metadata/android/zh-CN/short_description.txt @@ -1 +1 @@ -群组消息应用-加密的消息传递、群组聊天和视频通话 +群组消息应用——加密的消息传递、群组聊天和视频通话 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104360.txt b/fastlane/metadata/android/zh-TW/changelogs/40104360.txt new file mode 100644 index 0000000000..be36b60840 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104360.txt @@ -0,0 +1,3 @@ +新的應用程式佈局可在「實驗室」設定中啟用。請試試看! +修復關於遺失通知的問題,以及增量同步需要長時間的問題。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/gradle.properties b/gradle.properties index 2af9214ed5..2c999af35d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,9 +12,11 @@ org.gradle.jvmargs=-Xmx4g -Xms512M -XX:MaxPermSize=2048m -XX:MaxMetaspaceSize=1g org.gradle.configureondemand=true org.gradle.parallel=true org.gradle.vfs.watch=true +org.gradle.caching=true # Android Settings android.enableJetifier=true +android.jetifier.ignorelist=android-base-common,common android.useAndroidX=true #Project Settings diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index 764cf8419a..98398760d1 100644 --- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -17,7 +17,6 @@ package im.vector.lib.attachmentviewer -import android.annotation.SuppressLint import android.graphics.Color import android.os.Build import android.os.Bundle @@ -136,7 +135,6 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi } } - @Suppress("DEPRECATION") private fun setDecorViewFullScreen() { // This is important for the dispatchTouchEvent, if not we must correct // the touch coordinates @@ -144,22 +142,20 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } else { - @SuppressLint("WrongConstant") - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE - } + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // New API instead of FLAG_TRANSLUCENT_STATUS window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) } else { + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE) + @Suppress("DEPRECATION") window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + @Suppress("DEPRECATION") window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) } } @@ -344,7 +340,6 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi ?.handleCommand(commands) } - @Suppress("DEPRECATION") private fun hideSystemUI() { systemUiVisibility = false // Enables regular immersive mode. @@ -356,17 +351,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } else { - @SuppressLint("WrongConstant") - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE - } + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // New API instead of FLAG_TRANSLUCENT_STATUS window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_NAVIGATION window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) } else { + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE // Set the content to appear under the system bars so that the // content doesn't resize when the system bars hide and show. @@ -381,13 +372,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // Shows the system bars by removing all the flags // except for the ones that make the content appear under the system bars. - @Suppress("DEPRECATION") private fun showSystemUI() { systemUiVisibility = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION window.setDecorFitsSystemWindows(false) } else { + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) diff --git a/library/external/dialpad/build.gradle b/library/external/dialpad/build.gradle new file mode 100644 index 0000000000..fade8ddf30 --- /dev/null +++ b/library/external/dialpad/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation libs.androidx.appCompat +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/dialpad/src/main/AndroidManifest.xml b/library/external/dialpad/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1d412d0ae5 --- /dev/null +++ b/library/external/dialpad/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/library/external/dialpad/src/main/java/com/android/dialer/animation/AnimUtils.java b/library/external/dialpad/src/main/java/com/android/dialer/animation/AnimUtils.java new file mode 100644 index 0000000000..b6a32c587c --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/animation/AnimUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.android.dialer.animation; + +import android.view.animation.Interpolator; + +import com.android.dialer.compat.PathInterpolatorCompat; + +public class AnimUtils { + public static final Interpolator EASE_OUT_EASE_IN = + PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1); +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/compat/PathInterpolatorCompat.java b/library/external/dialpad/src/main/java/com/android/dialer/compat/PathInterpolatorCompat.java new file mode 100644 index 0000000000..7139bc4af1 --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/compat/PathInterpolatorCompat.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.dialer.compat; + +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.os.Build; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +public class PathInterpolatorCompat { + + public static Interpolator create( + float controlX1, float controlY1, float controlX2, float controlY2) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return new PathInterpolator(controlX1, controlY1, controlX2, controlY2); + } + return new PathInterpolatorBase(controlX1, controlY1, controlX2, controlY2); + } + + private static class PathInterpolatorBase implements Interpolator { + + /** Governs the accuracy of the approximation of the {@link Path}. */ + private static final float PRECISION = 0.002f; + + private final float[] mX; + private final float[] mY; + + public PathInterpolatorBase(Path path) { + final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */); + + final float pathLength = pathMeasure.getLength(); + final int numPoints = (int) (pathLength / PRECISION) + 1; + + mX = new float[numPoints]; + mY = new float[numPoints]; + + final float[] position = new float[2]; + for (int i = 0; i < numPoints; ++i) { + final float distance = (i * pathLength) / (numPoints - 1); + pathMeasure.getPosTan(distance, position, null /* tangent */); + + mX[i] = position[0]; + mY[i] = position[1]; + } + } + + public PathInterpolatorBase(float controlX, float controlY) { + this(createQuad(controlX, controlY)); + } + + public PathInterpolatorBase( + float controlX1, float controlY1, float controlX2, float controlY2) { + this(createCubic(controlX1, controlY1, controlX2, controlY2)); + } + + private static Path createQuad(float controlX, float controlY) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.quadTo(controlX, controlY, 1.0f, 1.0f); + return path; + } + + private static Path createCubic( + float controlX1, float controlY1, float controlX2, float controlY2) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0f, 1.0f); + return path; + } + + @Override + public float getInterpolation(float t) { + if (t <= 0.0f) { + return 0.0f; + } else if (t >= 1.0f) { + return 1.0f; + } + + // Do a binary search for the correct x to interpolate between. + int startIndex = 0; + int endIndex = mX.length - 1; + while (endIndex - startIndex > 1) { + int midIndex = (startIndex + endIndex) / 2; + if (t < mX[midIndex]) { + endIndex = midIndex; + } else { + startIndex = midIndex; + } + } + + final float xRange = mX[endIndex] - mX[startIndex]; + if (xRange == 0) { + return mY[startIndex]; + } + + final float tInRange = t - mX[startIndex]; + final float fraction = tInRange / xRange; + + final float startY = mY[startIndex]; + final float endY = mY[endIndex]; + + return startY + (fraction * (endY - startY)); + } + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadKeyButton.java b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadKeyButton.java new file mode 100644 index 0000000000..de6d2c6282 --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadKeyButton.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.dialer.dialpadview; + +import android.content.Context; +import android.graphics.RectF; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; + +/** + * Custom class for dialpad buttons. + * + *

When touch exploration mode is enabled for accessibility, this class implements the + * lift-to-type interaction model: + * + *

    + *
  • Hovering over the button will cause it to gain accessibility focus + *
  • Removing the hover pointer while inside the bounds of the button will perform a click action + *
  • If long-click is supported, hovering over the button for a longer period of time will switch + * to the long-click action + *
  • Moving the hover pointer outside of the bounds of the button will restore to the normal click + * action + *
+ */ +public class DialpadKeyButton extends FrameLayout { + + /** Timeout before switching to long-click accessibility mode. */ + private static final int LONG_HOVER_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2; + + /** Accessibility manager instance used to check touch exploration state. */ + private AccessibilityManager mAccessibilityManager; + + /** Bounds used to filter HOVER_EXIT events. */ + private RectF mHoverBounds = new RectF(); + + /** Whether this view is currently in the long-hover state. */ + private boolean mLongHovered; + + /** Alternate content description for long-hover state. */ + private CharSequence mLongHoverContentDesc; + + /** Backup of standard content description. Used for accessibility. */ + private CharSequence mBackupContentDesc; + + /** Backup of clickable property. Used for accessibility. */ + private boolean mWasClickable; + + /** Backup of long-clickable property. Used for accessibility. */ + private boolean mWasLongClickable; + + /** Runnable used to trigger long-click mode for accessibility. */ + private Runnable mLongHoverRunnable; + + private OnPressedListener mOnPressedListener; + + public DialpadKeyButton(Context context, AttributeSet attrs) { + super(context, attrs); + initForAccessibility(context); + } + + public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initForAccessibility(context); + } + + public void setOnPressedListener(OnPressedListener onPressedListener) { + mOnPressedListener = onPressedListener; + } + + private void initForAccessibility(Context context) { + mAccessibilityManager = + (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + public void setLongHoverContentDescription(CharSequence contentDescription) { + mLongHoverContentDesc = contentDescription; + + if (mLongHovered) { + super.setContentDescription(mLongHoverContentDesc); + } + } + + @Override + public void setContentDescription(CharSequence contentDescription) { + if (mLongHovered) { + mBackupContentDesc = contentDescription; + } else { + super.setContentDescription(contentDescription); + } + } + + @Override + public void setPressed(boolean pressed) { + super.setPressed(pressed); + if (mOnPressedListener != null) { + mOnPressedListener.onPressed(this, pressed); + } + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + mHoverBounds.left = getPaddingLeft(); + mHoverBounds.right = w - getPaddingRight(); + mHoverBounds.top = getPaddingTop(); + mHoverBounds.bottom = h - getPaddingBottom(); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + simulateClickForAccessibility(); + return true; + } + + return super.performAccessibilityAction(action, arguments); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + // When touch exploration is turned on, lifting a finger while inside + // the button's hover target bounds should perform a click action. + if (mAccessibilityManager.isEnabled() && mAccessibilityManager.isTouchExplorationEnabled()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + // Lift-to-type temporarily disables double-tap activation. + mWasClickable = isClickable(); + mWasLongClickable = isLongClickable(); + if (mWasLongClickable && mLongHoverContentDesc != null) { + if (mLongHoverRunnable == null) { + mLongHoverRunnable = + new Runnable() { + @Override + public void run() { + setLongHovered(true); + announceForAccessibility(mLongHoverContentDesc); + } + }; + } + postDelayed(mLongHoverRunnable, LONG_HOVER_TIMEOUT); + } + + setClickable(false); + setLongClickable(false); + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mHoverBounds.contains(event.getX(), event.getY())) { + if (mLongHovered) { + performLongClick(); + } else { + simulateClickForAccessibility(); + } + } + + cancelLongHover(); + setClickable(mWasClickable); + setLongClickable(mWasLongClickable); + break; + } + } + + return super.onHoverEvent(event); + } + + /** + * When accessibility is on, simulate press and release to preserve the semantic meaning of + * performClick(). Required for Braille support. + */ + private void simulateClickForAccessibility() { + // Checking the press state prevents double activation. + if (isPressed()) { + return; + } + + setPressed(true); + + // Stay consistent with performClick() by sending the event after + // setting the pressed state but before performing the action. + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + + setPressed(false); + } + + private void setLongHovered(boolean enabled) { + if (mLongHovered != enabled) { + mLongHovered = enabled; + + // Switch between normal and alternate description, if available. + if (enabled) { + mBackupContentDesc = getContentDescription(); + super.setContentDescription(mLongHoverContentDesc); + } else { + super.setContentDescription(mBackupContentDesc); + } + } + } + + private void cancelLongHover() { + if (mLongHoverRunnable != null) { + removeCallbacks(mLongHoverRunnable); + } + setLongHovered(false); + } + + public interface OnPressedListener { + + void onPressed(View view, boolean pressed); + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadTextView.java b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadTextView.java new file mode 100644 index 0000000000..5b1b7bb5dc --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadTextView.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.android.dialer.dialpadview; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * This is a custom text view intended only for rendering the numerals (and star and pound) on the + * dialpad. TextView has built in top/bottom padding to help account for ascenders/descenders. + * + *

Since vertical space is at a premium on the dialpad, particularly if the font size is scaled + * to a larger default, for the dialpad we use this class to more precisely render characters + * according to the precise amount of space they need. + */ +public class DialpadTextView extends TextView { + + private Rect mTextBounds = new Rect(); + private String mTextStr; + + public DialpadTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** Draw the text to fit within the height/width which have been specified during measurement. */ + @Override + public void draw(Canvas canvas) { + Paint paint = getPaint(); + + // Without this, the draw does not respect the style's specified text color. + paint.setColor(getCurrentTextColor()); + + // The text bounds values are relative and can be negative,, so rather than specifying a + // standard origin such as 0, 0, we need to use negative of the left/top bounds. + // For example, the bounds may be: Left: 11, Right: 37, Top: -77, Bottom: 0 + canvas.drawText(mTextStr, -mTextBounds.left, -mTextBounds.top, paint); + } + + /** + * Calculate the pixel-accurate bounds of the text when rendered, and use that to specify the + * height and width. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mTextStr = getText().toString(); + getPaint().getTextBounds(mTextStr, 0, mTextStr.length(), mTextBounds); + + int width = resolveSize(mTextBounds.width(), widthMeasureSpec); + int height = resolveSize(mTextBounds.height(), heightMeasureSpec); + setMeasuredDimension(width, height); + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadView.java b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadView.java new file mode 100644 index 0000000000..5c6ce46257 --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DialpadView.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.android.dialer.dialpadview; + +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.accessibility.AccessibilityManager; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.dialer.animation.AnimUtils; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +/** View that displays a twelve-key phone dialpad. */ +public class DialpadView extends LinearLayout { + + private static final String TAG = DialpadView.class.getSimpleName(); + + private static final double DELAY_MULTIPLIER = 0.66; + private static final double DURATION_MULTIPLIER = 0.8; + // For animation. + private static final int KEY_FRAME_DURATION = 33; + /** {@code True} if the dialpad is in landscape orientation. */ + private final boolean mIsLandscape; + /** {@code True} if the dialpad is showing in a right-to-left locale. */ + private final boolean mIsRtl; + + private final int[] mButtonIds = + new int[] { + R.id.zero, + R.id.one, + R.id.two, + R.id.three, + R.id.four, + R.id.five, + R.id.six, + R.id.seven, + R.id.eight, + R.id.nine, + R.id.star, + R.id.pound + }; + private EditText mDigits; + private ImageButton mDelete; + private View mOverflowMenuButton; + private ViewGroup mRateContainer; + private TextView mIldCountry; + private TextView mIldRate; + private boolean mCanDigitsBeEdited; + private int mTranslateDistance; + + public DialpadView(Context context) { + this(context, null); + } + + public DialpadView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DialpadView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mTranslateDistance = + getResources().getDimensionPixelSize(R.dimen.dialpad_key_button_translate_y); + + mIsLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + mIsRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && + TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + } + + @Override + protected void onFinishInflate() { + setupKeypad(); + mDigits = (EditText) findViewById(R.id.digits); + mDelete = (ImageButton) findViewById(R.id.deleteButton); + mOverflowMenuButton = findViewById(R.id.dialpad_overflow); + mRateContainer = (ViewGroup) findViewById(R.id.rate_container); + mIldCountry = (TextView) mRateContainer.findViewById(R.id.ild_country); + mIldRate = (TextView) mRateContainer.findViewById(R.id.ild_rate); + + AccessibilityManager accessibilityManager = + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + // The text view must be selected to send accessibility events. + mDigits.setSelected(true); + } + } + + private void setupKeypad() { + final int[] letterIds = + new int[] { + R.string.dialpad_0_letters, + R.string.dialpad_1_letters, + R.string.dialpad_2_letters, + R.string.dialpad_3_letters, + R.string.dialpad_4_letters, + R.string.dialpad_5_letters, + R.string.dialpad_6_letters, + R.string.dialpad_7_letters, + R.string.dialpad_8_letters, + R.string.dialpad_9_letters, + R.string.dialpad_star_letters, + R.string.dialpad_pound_letters + }; + + final Resources resources = getContext().getResources(); + + DialpadKeyButton dialpadKey; + TextView numberView; + TextView lettersView; + + final Locale currentLocale = resources.getConfiguration().locale; + final NumberFormat nf; + // We translate dialpad numbers only for "fa" and not any other locale + // ("ar" anybody ?). + if ("fa".equals(currentLocale.getLanguage())) { + nf = DecimalFormat.getInstance(resources.getConfiguration().locale); + } else { + nf = DecimalFormat.getInstance(Locale.ENGLISH); + } + + for (int i = 0; i < mButtonIds.length; i++) { + dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]); + numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number); + lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters); + + final String numberString; + final CharSequence numberContentDescription; + if (mButtonIds[i] == R.id.pound) { + numberString = resources.getString(R.string.dialpad_pound_number); + numberContentDescription = numberString; + } else if (mButtonIds[i] == R.id.star) { + numberString = resources.getString(R.string.dialpad_star_number); + numberContentDescription = numberString; + } else { + numberString = nf.format(i); + // The content description is used for Talkback key presses. The number is + // separated by a "," to introduce a slight delay. Convert letters into a verbatim + // span so that they are read as letters instead of as one word. + String letters = resources.getString(letterIds[i]); + Spannable spannable = + Spannable.Factory.getInstance().newSpannable(numberString + "," + letters); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + spannable.setSpan( + (new TtsSpan.VerbatimBuilder(letters)).build(), + numberString.length() + 1, + numberString.length() + 1 + letters.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + numberContentDescription = spannable; + } + + numberView.setText(numberString); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + numberView.setElegantTextHeight(false); + } + dialpadKey.setContentDescription(numberContentDescription); + + if (lettersView != null) { + lettersView.setText(resources.getString(letterIds[i])); + } + } + + final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one); + one.setLongHoverContentDescription(resources.getText(R.string.description_voicemail_button)); + + final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero); + zero.setLongHoverContentDescription(resources.getText(R.string.description_image_button_plus)); + } + + private Drawable getDrawableCompat(Context context, int id) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return context.getDrawable(id); + } else { + return context.getResources().getDrawable(id); + } + } + + public void setShowVoicemailButton(boolean show) { + View view = findViewById(R.id.dialpad_key_voicemail); + if (view != null) { + view.setVisibility(show ? View.VISIBLE : View.INVISIBLE); + } + } + + /** + * Whether or not the digits above the dialer can be edited. + * + * @param canBeEdited If true, the backspace button will be shown and the digits EditText will be + * configured to allow text manipulation. + */ + public void setCanDigitsBeEdited(boolean canBeEdited) { +// View deleteButton = findViewById(R.id.deleteButton); +// deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.INVISIBLE); +// View overflowMenuButton = findViewById(R.id.dialpad_overflow); +// overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE); + +// EditText digits = (EditText) findViewById(R.id.digits); +// digits.setClickable(canBeEdited); +// digits.setLongClickable(canBeEdited); +// digits.setFocusableInTouchMode(canBeEdited); +// digits.setCursorVisible(false); + + mCanDigitsBeEdited = canBeEdited; + } + + public void setCallRateInformation(String countryName, String displayRate) { + if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) { + mRateContainer.setVisibility(View.GONE); + return; + } + mRateContainer.setVisibility(View.VISIBLE); + mIldCountry.setText(countryName); + mIldRate.setText(displayRate); + } + + public boolean canDigitsBeEdited() { + return mCanDigitsBeEdited; + } + + /** + * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to the + * dialpad overlaying other fragments. + */ + @Override + public boolean onHoverEvent(MotionEvent event) { + return true; + } + + public void animateShow() { + // This is a hack; without this, the setTranslationY is delayed in being applied, and the + // numbers appear at their original position (0) momentarily before animating. + final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {}; + + for (int i = 0; i < mButtonIds.length; i++) { + int delay = (int) (getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER); + int duration = (int) (getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER); + final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]); + + ViewPropertyAnimator animator = dialpadKey.animate(); + if (mIsLandscape) { + // Landscape orientation requires translation along the X axis. + // For RTL locales, ensure we translate negative on the X axis. + dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance); + animator.translationX(0); + } else { + // Portrait orientation requires translation along the Y axis. + dialpadKey.setTranslationY(mTranslateDistance); + animator.translationY(0); + } + animator + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setStartDelay(delay) + .setDuration(duration) + .setListener(showListener) + .start(); + } + } + + public EditText getDigits() { + return mDigits; + } + + public ImageButton getDeleteButton() { + return mDelete; + } + + public View getOverflowMenuButton() { + return mOverflowMenuButton; + } + + /** + * Get the animation delay for the buttons, taking into account whether the dialpad is in + * landscape left-to-right, landscape right-to-left, or portrait. + * + * @param buttonId The button ID. + * @return The animation delay. + */ + private int getKeyButtonAnimationDelay(int buttonId) { + if (mIsLandscape) { + if (mIsRtl) { + if (buttonId == R.id.three) { + return KEY_FRAME_DURATION * 1; + } else if (buttonId == R.id.six) { + return KEY_FRAME_DURATION * 2; + } else if (buttonId == R.id.nine) { + return KEY_FRAME_DURATION * 3; + } else if (buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 4; + } else if (buttonId == R.id.two) { + return KEY_FRAME_DURATION * 5; + } else if (buttonId == R.id.five) { + return KEY_FRAME_DURATION * 6; + } else if (buttonId == R.id.eight) { + return KEY_FRAME_DURATION * 7; + } else if (buttonId == R.id.zero) { + return KEY_FRAME_DURATION * 8; + } else if (buttonId == R.id.one) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.four) { + return KEY_FRAME_DURATION * 10; + } else if (buttonId == R.id.seven || buttonId == R.id.star) { + return KEY_FRAME_DURATION * 11; + } + } else { + if (buttonId == R.id.one) { + return KEY_FRAME_DURATION * 1; + } else if (buttonId == R.id.four) { + return KEY_FRAME_DURATION * 2; + } else if (buttonId == R.id.seven) { + return KEY_FRAME_DURATION * 3; + } else if (buttonId == R.id.star) { + return KEY_FRAME_DURATION * 4; + } else if (buttonId == R.id.two) { + return KEY_FRAME_DURATION * 5; + } else if (buttonId == R.id.five) { + return KEY_FRAME_DURATION * 6; + } else if (buttonId == R.id.eight) { + return KEY_FRAME_DURATION * 7; + } else if (buttonId == R.id.zero) { + return KEY_FRAME_DURATION * 8; + } else if (buttonId == R.id.three) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.six) { + return KEY_FRAME_DURATION * 10; + } else if (buttonId == R.id.nine || buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 11; + } + } + } else { + if (buttonId == R.id.one) { + return KEY_FRAME_DURATION * 1; + } else if (buttonId == R.id.two) { + return KEY_FRAME_DURATION * 2; + } else if (buttonId == R.id.three) { + return KEY_FRAME_DURATION * 3; + } else if (buttonId == R.id.four) { + return KEY_FRAME_DURATION * 4; + } else if (buttonId == R.id.five) { + return KEY_FRAME_DURATION * 5; + } else if (buttonId == R.id.six) { + return KEY_FRAME_DURATION * 6; + } else if (buttonId == R.id.seven) { + return KEY_FRAME_DURATION * 7; + } else if (buttonId == R.id.eight) { + return KEY_FRAME_DURATION * 8; + } else if (buttonId == R.id.nine) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.star) { + return KEY_FRAME_DURATION * 10; + } else if (buttonId == R.id.zero || buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 11; + } + } + + Log.wtf(TAG, "Attempted to get animation delay for invalid key button id."); + return 0; + } + + /** + * Get the button animation duration, taking into account whether the dialpad is in landscape + * left-to-right, landscape right-to-left, or portrait. + * + * @param buttonId The button ID. + * @return The animation duration. + */ + private int getKeyButtonAnimationDuration(int buttonId) { + if (mIsLandscape) { + if (mIsRtl) { + if (buttonId == R.id.one + || buttonId == R.id.four + || buttonId == R.id.seven + || buttonId == R.id.star) { + return KEY_FRAME_DURATION * 8; + } else if (buttonId == R.id.two + || buttonId == R.id.five + || buttonId == R.id.eight + || buttonId == R.id.zero) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.three + || buttonId == R.id.six + || buttonId == R.id.nine + || buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 10; + } + } else { + if (buttonId == R.id.one + || buttonId == R.id.four + || buttonId == R.id.seven + || buttonId == R.id.star) { + return KEY_FRAME_DURATION * 10; + } else if (buttonId == R.id.two + || buttonId == R.id.five + || buttonId == R.id.eight + || buttonId == R.id.zero) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.three + || buttonId == R.id.six + || buttonId == R.id.nine + || buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 8; + } + } + } else { + if (buttonId == R.id.one + || buttonId == R.id.two + || buttonId == R.id.three + || buttonId == R.id.four + || buttonId == R.id.five + || buttonId == R.id.six) { + return KEY_FRAME_DURATION * 10; + } else if (buttonId == R.id.seven || buttonId == R.id.eight || buttonId == R.id.nine) { + return KEY_FRAME_DURATION * 9; + } else if (buttonId == R.id.star || buttonId == R.id.zero || buttonId == R.id.pound) { + return KEY_FRAME_DURATION * 8; + } + } + + Log.wtf(TAG, "Attempted to get animation duration for invalid key button id."); + return 0; + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DigitsEditText.java b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DigitsEditText.java new file mode 100644 index 0000000000..053b301eed --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/dialpadview/DigitsEditText.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.android.dialer.dialpadview; + +import android.content.Context; +import android.graphics.Rect; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethodManager; + +import com.android.dialer.widget.ResizingTextEditText; + +/** EditText which suppresses IME show up. */ +public class DigitsEditText extends ResizingTextEditText { + private OnTextContextMenuClickListener mOnTextContextMenuClickListener; + + public DigitsEditText(Context context, AttributeSet attrs) { + super(context, attrs); + setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + setShowSoftInputOnFocus(false); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + final InputMethodManager imm = + ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); + if (imm != null && imm.isActive(this)) { + imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final boolean ret = super.onTouchEvent(event); + // Must be done after super.onTouchEvent() + final InputMethodManager imm = + ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); + if (imm != null && imm.isActive(this)) { + imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0); + } + return ret; + } + + @Override + protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + super.onTextChanged(text, start, lengthBefore, lengthAfter); + if (isCursorVisible()) { + setSelection(getText().length()); + } + } + + @Override + public boolean onTextContextMenuItem(int id) { + boolean value = super.onTextContextMenuItem(id); + if (mOnTextContextMenuClickListener != null) { + mOnTextContextMenuClickListener.onTextContextMenuClickListener(id); + } + return value; + } + + public interface OnTextContextMenuClickListener { + void onTextContextMenuClickListener(int id); + } + + public void setOnTextContextMenuClickListener(OnTextContextMenuClickListener listener) { + this.mOnTextContextMenuClickListener = listener; + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/util/ViewUtil.java b/library/external/dialpad/src/main/java/com/android/dialer/util/ViewUtil.java new file mode 100644 index 0000000000..4f6d1dd47c --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/util/ViewUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.dialer.util; + +import android.graphics.Paint; +import android.util.TypedValue; +import android.widget.TextView; + +/** Provides static functions to work with views */ +public class ViewUtil { + + private ViewUtil() {} + + public static void resizeText(TextView textView, int originalTextSize, int minTextSize) { + final Paint paint = textView.getPaint(); + final int width = textView.getWidth(); + if (width == 0) { + return; + } + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize); + float ratio = width / paint.measureText(textView.getText().toString()); + if (ratio <= 1.0f) { + textView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, Math.max(minTextSize, originalTextSize * ratio)); + } + } +} diff --git a/library/external/dialpad/src/main/java/com/android/dialer/widget/ResizingTextEditText.java b/library/external/dialpad/src/main/java/com/android/dialer/widget/ResizingTextEditText.java new file mode 100644 index 0000000000..216175981b --- /dev/null +++ b/library/external/dialpad/src/main/java/com/android/dialer/widget/ResizingTextEditText.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.android.dialer.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.EditText; +import com.android.dialer.dialpadview.R; +import com.android.dialer.util.ViewUtil; + +/** EditText which resizes dynamically with respect to text length. */ +public class ResizingTextEditText extends EditText { + + private final int mOriginalTextSize; + private final int mMinTextSize; + + public ResizingTextEditText(Context context, AttributeSet attrs) { + super(context, attrs); + mOriginalTextSize = (int) getTextSize(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResizingText); + mMinTextSize = + (int) a.getDimension(R.styleable.ResizingText_resizing_text_min_size, mOriginalTextSize); + a.recycle(); + } + + @Override + protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + super.onTextChanged(text, start, lengthBefore, lengthAfter); + ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize); + } +} diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..cd19726776 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_drop_down_white_18.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000..41541bb0d0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_backspace_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_backspace_white_24.png new file mode 100644 index 0000000000..136e8b8c1a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_backspace_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_block_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_block_white_24.png new file mode 100644 index 0000000000..2ccc89d246 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_block_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000..ec2349ca83 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000..398f0a938c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000..625b827c44 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000..51456d3d5d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_end_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_made_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_made_white_24.png new file mode 100644 index 0000000000..ea6a8ab5f2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_made_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_merge_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000..b7aba8072e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_merge_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_missed_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_missed_white_24.png new file mode 100644 index 0000000000..f188eb9aa5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_missed_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_received_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_received_white_24.png new file mode 100644 index 0000000000..ca2ae411a8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_received_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_18.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_18.png new file mode 100644 index 0000000000..0bdc56be6f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_24.png new file mode 100644 index 0000000000..4dc5065155 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_call_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000..497c88ca82 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_48.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000..c8e69dcebb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_camera_alt_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_black_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_black_24.png new file mode 100644 index 0000000000..e802d90aeb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_circle_googblue_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_circle_googblue_24.png new file mode 100644 index 0000000000..52ff857ba8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_check_circle_googblue_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_close_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_close_white_24.png new file mode 100644 index 0000000000..ceb1a1eebf Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_close_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..6acef1745d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_delete_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_delete_white_24.png new file mode 100644 index 0000000000..8444f31384 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_delete_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_24.png new file mode 100644 index 0000000000..9037f94e84 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000..82710e72a5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_dialpad_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_edit_grey600_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_edit_grey600_24.png new file mode 100644 index 0000000000..4a27b46968 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_edit_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_forward_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_forward_white_24.png new file mode 100644 index 0000000000..a0711d377e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_forward_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_exit_white_48.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000..159bea7fd8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_white_48.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000..9b8131124d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_fullscreen_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_grade_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_grade_white_24.png new file mode 100644 index 0000000000..86eecdd4a0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_grade_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_group_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_group_white_36.png new file mode 100644 index 0000000000..f98a074ac1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_group_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_hd_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_hd_white_24.png new file mode 100644 index 0000000000..35bf51a4f1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_hd_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_grey600_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000..e859c2f31a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000..f77f24767c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_headset_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_history_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_history_white_24.png new file mode 100644 index 0000000000..485c826fdf Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_history_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_image_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_image_white_24.png new file mode 100644 index 0000000000..b414cf5b68 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_image_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_info_outline_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_info_outline_white_24.png new file mode 100644 index 0000000000..c7b1113cfe Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_info_outline_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_message_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_message_white_24.png new file mode 100644 index 0000000000..57177b7c6f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_message_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_black_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000..1755dbf3fa Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000..203cb8a9ff Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_mic_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_more_vert_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_more_vert_white_24.png new file mode 100644 index 0000000000..58e092b8af Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_more_vert_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_network_wifi_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000..8df91f2367 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_network_wifi_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_24.png new file mode 100644 index 0000000000..4d2ea05c46 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000..1d024393aa Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_pause_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_people_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_people_white_24.png new file mode 100644 index 0000000000..25e443424e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_people_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_add_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000..10ae5a70c4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_add_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_white_24.png new file mode 100644 index 0000000000..56708b0bad Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_person_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_library_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000..c4a2229e94 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_library_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000..b414cf5b68 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_48.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000..f9f1defa6d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_photo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_play_arrow_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_play_arrow_white_24.png new file mode 100644 index 0000000000..57c9fa5460 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_play_arrow_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_18.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_18.png new file mode 100644 index 0000000000..f0bb6f5beb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_24.png new file mode 100644 index 0000000000..ff7d95706a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_36.png new file mode 100644 index 0000000000..057d9c757c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_report_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_schedule_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_schedule_white_24.png new file mode 100644 index 0000000000..4b7caa097c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_schedule_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_search_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_search_white_24.png new file mode 100644 index 0000000000..bbfbc96cbc Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_search_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..5d4ad4b020 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png new file mode 100644 index 0000000000..5a53192125 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_swap_calls_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000..8c3a0edaa3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_swap_calls_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..4366bb0827 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000..aaf5ac2085 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000..f2e461a9f1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_18.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000..abf478adaa Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000..d83e0d50c3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000..49562a6408 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_videocam_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_voicemail_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_voicemail_white_24.png new file mode 100644 index 0000000000..03a62e15f9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_voicemail_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_down_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_down_white_24.png new file mode 100644 index 0000000000..e22e92c857 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_down_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_grey600_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000..49eb8fcc34 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_24.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_24.png new file mode 100644 index 0000000000..57d787163e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_36.png b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000..62d22bec87 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-hdpi-v4/quantum_ic_volume_up_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..f517557627 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..90bf872ac8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..b8d4ce444b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..6c8174f3af Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-hdpi-v17/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..22a1140ae2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..01b869a608 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..7933f42f0a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..b47cef666e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-mdpi-v17/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..d858f18e6c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..831b5249cb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..4735a7d711 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..6a984c4f16 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xhdpi-v17/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..614ad49a3e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..71f3bd6838 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..4a9e2c24aa Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..907911055b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxhdpi-v17/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..d409b544b7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..3b2aed29b5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..0167ac8291 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..aa7a919430 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-ldrtl-xxxhdpi-v17/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..4ef72eec99 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_drop_down_white_18.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000..7c1fc3d7ca Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_backspace_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_backspace_white_24.png new file mode 100644 index 0000000000..48863dcdd7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_backspace_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_block_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_block_white_24.png new file mode 100644 index 0000000000..ec1b33f0ea Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_block_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000..de635e034d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000..046372d0df Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000..378272ffc1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000..625b827c44 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_end_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_made_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_made_white_24.png new file mode 100644 index 0000000000..9b3cd43803 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_made_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_merge_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000..a2eb54bab1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_merge_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_missed_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_missed_white_24.png new file mode 100644 index 0000000000..42c360b8a2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_missed_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_received_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_received_white_24.png new file mode 100644 index 0000000000..fbc1e86e24 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_received_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_18.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_18.png new file mode 100644 index 0000000000..bd5748575f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_24.png new file mode 100644 index 0000000000..77f9de5e3c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_call_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000..e830522008 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_48.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000..be9fb226a5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_camera_alt_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_black_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_black_24.png new file mode 100644 index 0000000000..1c14c9c445 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_circle_googblue_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_circle_googblue_24.png new file mode 100644 index 0000000000..eff5627225 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_check_circle_googblue_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_close_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_close_white_24.png new file mode 100644 index 0000000000..af7f8288da Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_close_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..8ac80b083f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_delete_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_delete_white_24.png new file mode 100644 index 0000000000..e2268c9bed Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_delete_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_24.png new file mode 100644 index 0000000000..6c405f9ba9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000..9037f94e84 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_dialpad_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_edit_grey600_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_edit_grey600_24.png new file mode 100644 index 0000000000..f003bc9d33 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_edit_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_forward_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_forward_white_24.png new file mode 100644 index 0000000000..65f73299f9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_forward_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_exit_white_48.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000..364bad0b84 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_white_48.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000..4423c7ce99 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_fullscreen_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_grade_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_grade_white_24.png new file mode 100644 index 0000000000..d2cbe4c92b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_grade_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_group_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_group_white_36.png new file mode 100644 index 0000000000..25e443424e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_group_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_hd_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_hd_white_24.png new file mode 100644 index 0000000000..30938fe4d4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_hd_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_grey600_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000..371efd3822 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000..d25d3888e1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_headset_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_history_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_history_white_24.png new file mode 100644 index 0000000000..d67647c560 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_history_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_image_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_image_white_24.png new file mode 100644 index 0000000000..d474bd577d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_image_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_info_outline_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_info_outline_white_24.png new file mode 100644 index 0000000000..353e064951 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_info_outline_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_message_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_message_white_24.png new file mode 100644 index 0000000000..3072b75699 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_message_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_black_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000..da605a5a19 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000..6fccf5d09f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_mic_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_more_vert_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_more_vert_white_24.png new file mode 100644 index 0000000000..5ec0116f05 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_more_vert_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_network_wifi_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000..1c3e8b9879 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_network_wifi_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_24.png new file mode 100644 index 0000000000..2272d478c3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000..4d2ea05c46 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_pause_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_people_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_people_white_24.png new file mode 100644 index 0000000000..3f20e75533 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_people_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_add_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000..38e0a2882a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_add_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_white_24.png new file mode 100644 index 0000000000..f0b1c725da Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_person_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_library_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000..02ef4cdb00 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_library_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000..d474bd577d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_48.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000..2642b9e09e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_photo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_play_arrow_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_play_arrow_white_24.png new file mode 100644 index 0000000000..c61e948bbf Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_play_arrow_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_18.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_18.png new file mode 100644 index 0000000000..63ef736834 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_24.png new file mode 100644 index 0000000000..ac0f3948db Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_36.png new file mode 100644 index 0000000000..ff7d95706a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_report_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_schedule_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_schedule_white_24.png new file mode 100644 index 0000000000..f69736faa6 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_schedule_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_search_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_search_white_24.png new file mode 100644 index 0000000000..faefc59c8e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_search_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..b58afb0b49 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png new file mode 100644 index 0000000000..dd5a42f55c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_swap_calls_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000..9491f2d1af Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_swap_calls_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..b67f6a9116 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000..d1cca6f0a0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000..aaf5ac2085 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_18.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000..1dafd49276 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000..d146209a51 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000..d83e0d50c3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_videocam_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_voicemail_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_voicemail_white_24.png new file mode 100644 index 0000000000..e5aa7db055 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_voicemail_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_down_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_down_white_24.png new file mode 100644 index 0000000000..10992ed70c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_down_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_grey600_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000..d6cea3667a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_24.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_24.png new file mode 100644 index 0000000000..7cfd4c7b88 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_36.png b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000..57d787163e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-mdpi-v4/quantum_ic_volume_up_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-v21/btn_dialpad_key.xml b/library/external/dialpad/src/main/res/drawable-v21/btn_dialpad_key.xml new file mode 100644 index 0000000000..50614f9bed --- /dev/null +++ b/library/external/dialpad/src/main/res/drawable-v21/btn_dialpad_key.xml @@ -0,0 +1,18 @@ + + + + diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..832f5a3617 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_drop_down_white_18.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000..4c6076df77 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_backspace_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_backspace_white_24.png new file mode 100644 index 0000000000..ec5412bd8a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_backspace_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_block_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_block_white_24.png new file mode 100644 index 0000000000..7aba97b659 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_block_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000..eea1bbf04b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000..d5022d063e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000..a4fe6889d1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000..e1831d7afd Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_end_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_made_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_made_white_24.png new file mode 100644 index 0000000000..7fe6941051 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_made_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_merge_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000..01daecf656 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_merge_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_missed_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_missed_white_24.png new file mode 100644 index 0000000000..dd64489aae Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_missed_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_received_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_received_white_24.png new file mode 100644 index 0000000000..807308d9de Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_received_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_18.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_18.png new file mode 100644 index 0000000000..4dc5065155 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_24.png new file mode 100644 index 0000000000..ef45e933a9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_call_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000..be9fb226a5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_48.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000..777658e955 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_camera_alt_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_black_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_black_24.png new file mode 100644 index 0000000000..64a4944f75 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_circle_googblue_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_circle_googblue_24.png new file mode 100644 index 0000000000..e31fcf3507 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_check_circle_googblue_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_close_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_close_white_24.png new file mode 100644 index 0000000000..b7c7ffd0e7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_close_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..ca62598599 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_delete_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_delete_white_24.png new file mode 100644 index 0000000000..484260a971 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_delete_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_24.png new file mode 100644 index 0000000000..0e89f6c74b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000..175000510d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_dialpad_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_edit_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_edit_grey600_24.png new file mode 100644 index 0000000000..b5b3a243c7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_edit_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_forward_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_forward_white_24.png new file mode 100644 index 0000000000..7a5df52bf0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_forward_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_exit_white_48.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000..ef360fe40c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_white_48.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000..c1dcfb2902 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_fullscreen_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_grade_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_grade_white_24.png new file mode 100644 index 0000000000..d65f39d7cc Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_grade_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_group_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_group_white_36.png new file mode 100644 index 0000000000..7f0b7e903b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_group_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_hd_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_hd_white_24.png new file mode 100644 index 0000000000..4c954d86f8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_hd_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000..f7dbee156b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000..82db5427b7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_headset_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_history_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_history_white_24.png new file mode 100644 index 0000000000..3e73b49ee5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_history_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_image_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_image_white_24.png new file mode 100644 index 0000000000..2642b9e09e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_image_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_info_outline_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_info_outline_white_24.png new file mode 100644 index 0000000000..c571b2e3e7 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_info_outline_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_message_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_message_white_24.png new file mode 100644 index 0000000000..763767b4f6 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_message_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_black_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000..fa741be1c0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000..7a15a9ea9e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_mic_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_more_vert_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_more_vert_white_24.png new file mode 100644 index 0000000000..96e5d4321c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_more_vert_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_network_wifi_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000..ca927f3de5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_network_wifi_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_24.png new file mode 100644 index 0000000000..f49aed7571 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000..7192ad487e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_pause_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_people_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_people_white_24.png new file mode 100644 index 0000000000..715b49a3c8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_people_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_add_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000..7e7c289d49 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_add_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_white_24.png new file mode 100644 index 0000000000..aea15f0be5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_person_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_library_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000..4bd2898a83 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_library_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000..2642b9e09e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_48.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000..2ffdb55f26 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_photo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_play_arrow_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_play_arrow_white_24.png new file mode 100644 index 0000000000..a3c80e73da Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_play_arrow_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_18.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_18.png new file mode 100644 index 0000000000..dc0c995c17 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_24.png new file mode 100644 index 0000000000..74fc594aab Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_36.png new file mode 100644 index 0000000000..26b9172e8f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_report_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_schedule_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_schedule_white_24.png new file mode 100644 index 0000000000..1749ea2758 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_schedule_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_search_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_search_white_24.png new file mode 100644 index 0000000000..bfc3e39394 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_search_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..ef59e77678 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png new file mode 100644 index 0000000000..28b5afa9d4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_swap_calls_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000..698cd5d756 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_swap_calls_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..a5e719cdfb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000..5d540589b4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000..69565f2c75 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_18.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000..d83e0d50c3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000..1b2583d34e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000..44c28e2f28 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_videocam_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_voicemail_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_voicemail_white_24.png new file mode 100644 index 0000000000..59126d7066 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_voicemail_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_down_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_down_white_24.png new file mode 100644 index 0000000000..2621bc15d3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_down_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000..a45093ff79 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_24.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_24.png new file mode 100644 index 0000000000..2ed00343b8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_36.png b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000..2e751a40f5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xhdpi-v4/quantum_ic_volume_up_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..32a6d91ce8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000..2609ae1341 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_backspace_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_backspace_white_24.png new file mode 100644 index 0000000000..f6a90accff Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_backspace_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_block_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_block_white_24.png new file mode 100644 index 0000000000..fddfa54b85 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_block_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000..99f57c12a8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000..6842da6d0a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000..e1831d7afd Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000..13ffc2ad75 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_end_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_made_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_made_white_24.png new file mode 100644 index 0000000000..ae471c9fc5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_made_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_merge_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000..cefef6551b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_merge_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_missed_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_missed_white_24.png new file mode 100644 index 0000000000..2374dc5a11 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_missed_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_received_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_received_white_24.png new file mode 100644 index 0000000000..58421114fd Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_received_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_18.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_18.png new file mode 100644 index 0000000000..6f4dcea1f3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_24.png new file mode 100644 index 0000000000..90ead2e455 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_call_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000..c8e69dcebb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_48.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000..a4e7aea72d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_camera_alt_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_black_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_black_24.png new file mode 100644 index 0000000000..b26a2c05e3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_circle_googblue_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_circle_googblue_24.png new file mode 100644 index 0000000000..a8eb2a45ec Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_check_circle_googblue_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_close_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_close_white_24.png new file mode 100644 index 0000000000..6b717e0dda Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_close_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..c480ba78fe Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_delete_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_delete_white_24.png new file mode 100644 index 0000000000..603f28cbd1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_delete_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_24.png new file mode 100644 index 0000000000..175000510d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000..54ebbafaeb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_dialpad_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_edit_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_edit_grey600_24.png new file mode 100644 index 0000000000..f1f9ffce89 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_edit_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_forward_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_forward_white_24.png new file mode 100644 index 0000000000..7bd5b1635b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_forward_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000..b7f4133fd9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_white_48.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000..a0a1b4d4f3 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_fullscreen_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_grade_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_grade_white_24.png new file mode 100644 index 0000000000..aa5879215e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_grade_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_group_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_group_white_36.png new file mode 100644 index 0000000000..952e15fa69 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_group_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_hd_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_hd_white_24.png new file mode 100644 index 0000000000..dd08bbbecc Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_hd_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000..de1739bf4f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000..a0d8b14c04 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_headset_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_history_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_history_white_24.png new file mode 100644 index 0000000000..1358a129cf Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_history_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_image_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_image_white_24.png new file mode 100644 index 0000000000..f9f1defa6d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_image_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_info_outline_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_info_outline_white_24.png new file mode 100644 index 0000000000..c41a5fcffa Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_info_outline_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_message_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_message_white_24.png new file mode 100644 index 0000000000..0a79824b8f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_message_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_black_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000..084bf3c9f4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000..585d38326c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_mic_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_more_vert_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_more_vert_white_24.png new file mode 100644 index 0000000000..801ad89095 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_more_vert_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_network_wifi_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000..75469cd852 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_network_wifi_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_24.png new file mode 100644 index 0000000000..7192ad487e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000..a03bad27ed Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_pause_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_people_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_people_white_24.png new file mode 100644 index 0000000000..7f0b7e903b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_people_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_add_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000..8f744f0391 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_add_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_white_24.png new file mode 100644 index 0000000000..184f7418d5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_person_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_library_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000..497479291e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_library_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000..f9f1defa6d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_48.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000..3fe5c5ceb6 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_photo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_play_arrow_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_play_arrow_white_24.png new file mode 100644 index 0000000000..547ef30aac Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_play_arrow_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_18.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_18.png new file mode 100644 index 0000000000..919a872e0e Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_24.png new file mode 100644 index 0000000000..26b9172e8f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_36.png new file mode 100644 index 0000000000..2040c36d57 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_report_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_schedule_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_schedule_white_24.png new file mode 100644 index 0000000000..96df1fbac0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_schedule_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_search_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_search_white_24.png new file mode 100644 index 0000000000..abbb989510 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_search_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..0c5256413c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png new file mode 100644 index 0000000000..f4105ec8d1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_swap_calls_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000..140da28a8c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_swap_calls_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..8745f69ffc Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000..69565f2c75 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000..ff84832956 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_18.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000..49562a6408 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000..44c28e2f28 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000..839af26f82 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_videocam_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_voicemail_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_voicemail_white_24.png new file mode 100644 index 0000000000..28b8e936a0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_voicemail_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_down_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_down_white_24.png new file mode 100644 index 0000000000..5eb8b671f2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_down_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000..413b386524 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_24.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_24.png new file mode 100644 index 0000000000..2e751a40f5 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_36.png b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000..96c1f982fb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxhdpi-v4/quantum_ic_volume_up_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_back_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000..e27034d678 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_back_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000..c19c19d2bd Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_backspace_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_backspace_white_24.png new file mode 100644 index 0000000000..88131b9aff Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_backspace_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_block_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_block_white_24.png new file mode 100644 index 0000000000..0378d1bedc Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_block_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000..1595be1697 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000..3fe7c23502 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000..8801d0ded4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000..c8099a1a15 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_end_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_made_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_made_white_24.png new file mode 100644 index 0000000000..844ef86a07 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_made_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_merge_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000..9419ffbbc9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_merge_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_missed_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_missed_white_24.png new file mode 100644 index 0000000000..b1321a9aed Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_missed_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_received_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_received_white_24.png new file mode 100644 index 0000000000..417999c85a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_received_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_18.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_18.png new file mode 100644 index 0000000000..90ead2e455 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_24.png new file mode 100644 index 0000000000..b0e020573d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_call_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000..777658e955 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_48.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000..f2fe54bd51 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_camera_alt_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_black_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_black_24.png new file mode 100644 index 0000000000..2f6d6386de Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_circle_googblue_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_circle_googblue_24.png new file mode 100644 index 0000000000..7e08f61086 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_check_circle_googblue_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_close_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_close_white_24.png new file mode 100644 index 0000000000..3964192192 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_close_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_content_copy_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_content_copy_grey600_24.png new file mode 100644 index 0000000000..f0ea085c90 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_content_copy_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_delete_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_delete_white_24.png new file mode 100644 index 0000000000..c582dc2a49 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_delete_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_24.png new file mode 100644 index 0000000000..eb4307aeb2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000..a53aeb1d33 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_dialpad_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_edit_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_edit_grey600_24.png new file mode 100644 index 0000000000..a61298dbe6 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_edit_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_forward_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_forward_white_24.png new file mode 100644 index 0000000000..428009cfef Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_forward_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000..b47b3f8bdb Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_white_48.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000..ea9f18ae63 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_fullscreen_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_grade_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_grade_white_24.png new file mode 100644 index 0000000000..7f38d09639 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_grade_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_group_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_group_white_36.png new file mode 100644 index 0000000000..dacf299327 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_group_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_hd_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_hd_white_24.png new file mode 100644 index 0000000000..3f87b882ee Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_hd_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000..e968fa7d12 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000..89b9910476 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_headset_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_history_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_history_white_24.png new file mode 100644 index 0000000000..5b99ef6550 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_history_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_image_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_image_white_24.png new file mode 100644 index 0000000000..2ffdb55f26 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_image_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_info_outline_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_info_outline_white_24.png new file mode 100644 index 0000000000..3a82cab3b4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_info_outline_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_message_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_message_white_24.png new file mode 100644 index 0000000000..fa7c17ac45 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_message_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_black_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000..90d0606a45 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_black_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000..b0a10fbf67 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_mic_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_more_vert_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_more_vert_white_24.png new file mode 100644 index 0000000000..7a97f4cb70 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_more_vert_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_network_wifi_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000..eb284e3838 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_network_wifi_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_24.png new file mode 100644 index 0000000000..660ac65858 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000..3ea7e03e5d Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_pause_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_people_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_people_white_24.png new file mode 100644 index 0000000000..f52bd1ae59 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_people_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_add_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000..2fa2cca80c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_add_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_white_24.png new file mode 100644 index 0000000000..33d40d8b62 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_person_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_library_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000..8627f42767 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_library_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000..2ffdb55f26 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_48.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000..7d5091ded8 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_photo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_play_arrow_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_play_arrow_white_24.png new file mode 100644 index 0000000000..be5c062b5f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_play_arrow_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_18.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_18.png new file mode 100644 index 0000000000..aed7668042 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_24.png new file mode 100644 index 0000000000..023a56e76a Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_36.png new file mode 100644 index 0000000000..1912789d2f Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_report_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_schedule_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_schedule_white_24.png new file mode 100644 index 0000000000..19390a8bd2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_schedule_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_search_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_search_white_24.png new file mode 100644 index 0000000000..dd5adfc7f9 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_search_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_send_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_send_white_24.png new file mode 100644 index 0000000000..9dfa888c15 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_send_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png new file mode 100644 index 0000000000..58a4f9c945 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_signal_wifi_4_bar_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_swap_calls_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000..f8470b5dc0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_swap_calls_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_undo_white_48.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000..6d703c6ae2 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_undo_white_48.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000..bf37b57f9c Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000..7a915c30db Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_off_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_18.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000..44c28e2f28 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_18.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000..ed20c07062 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000..eff5923da4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_videocam_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_voicemail_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_voicemail_white_24.png new file mode 100644 index 0000000000..820ff5066b Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_voicemail_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_down_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_down_white_24.png new file mode 100644 index 0000000000..4ab55abbd1 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_down_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_grey600_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000..429dc02df0 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_grey600_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_24.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_24.png new file mode 100644 index 0000000000..82972b4e59 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_24.png differ diff --git a/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_36.png b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000..fd633b6cb4 Binary files /dev/null and b/library/external/dialpad/src/main/res/drawable-xxxhdpi-v4/quantum_ic_volume_up_white_36.png differ diff --git a/library/external/dialpad/src/main/res/drawable/btn_dialpad_key.xml b/library/external/dialpad/src/main/res/drawable/btn_dialpad_key.xml new file mode 100644 index 0000000000..10099df046 --- /dev/null +++ b/library/external/dialpad/src/main/res/drawable/btn_dialpad_key.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad.xml b/library/external/dialpad/src/main/res/layout/dialpad.xml new file mode 100644 index 0000000000..ec8450c7b5 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_fragment.xml b/library/external/dialpad/src/main/res/layout/dialpad_fragment.xml new file mode 100644 index 0000000000..4e9a5f330c --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_fragment.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_key.xml b/library/external/dialpad/src/main/res/layout/dialpad_key.xml new file mode 100644 index 0000000000..77e4fc53a6 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_key.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_key_one.xml b/library/external/dialpad/src/main/res/layout/dialpad_key_one.xml new file mode 100644 index 0000000000..2ef0baa1de --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_key_one.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_key_pound.xml b/library/external/dialpad/src/main/res/layout/dialpad_key_pound.xml new file mode 100644 index 0000000000..d37a6aa788 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_key_pound.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_key_star.xml b/library/external/dialpad/src/main/res/layout/dialpad_key_star.xml new file mode 100644 index 0000000000..d288475d01 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_key_star.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_key_zero.xml b/library/external/dialpad/src/main/res/layout/dialpad_key_zero.xml new file mode 100644 index 0000000000..943ae48dd2 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_key_zero.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_view.xml b/library/external/dialpad/src/main/res/layout/dialpad_view.xml new file mode 100644 index 0000000000..fb14ad0989 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_view.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/library/external/dialpad/src/main/res/layout/dialpad_view_unthemed.xml b/library/external/dialpad/src/main/res/layout/dialpad_view_unthemed.xml new file mode 100644 index 0000000000..1b7b78f907 --- /dev/null +++ b/library/external/dialpad/src/main/res/layout/dialpad_view_unthemed.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/external/dialpad/src/main/res/values-af/values-af.xml b/library/external/dialpad/src/main/res/values-af/values-af.xml new file mode 100644 index 0000000000..6a93515ac0 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-af/values-af.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Gaan terug" + "Meer opsies" + "plus" + "stemboodskap" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-am/values-am.xml b/library/external/dialpad/src/main/res/values-am/values-am.xml new file mode 100644 index 0000000000..08955ffeec --- /dev/null +++ b/library/external/dialpad/src/main/res/values-am/values-am.xml @@ -0,0 +1,8 @@ + + + "የኋሊት ደምሳሽ" + "ወደኋላ ያስሱ" + "ተጨማሪ አማራጮች" + "የመደመር ምልክት" + "የድምፅ መልዕክት" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ar/values-ar.xml b/library/external/dialpad/src/main/res/values-ar/values-ar.xml new file mode 100644 index 0000000000..e791d1e5f2 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ar/values-ar.xml @@ -0,0 +1,8 @@ + + + "مسافة للخلف" + "الرجوع" + "مزيد من الخيارات" + "علامة الجمع" + "بريد صوتي" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-az/values-az.xml b/library/external/dialpad/src/main/res/values-az/values-az.xml new file mode 100644 index 0000000000..152fdea46e --- /dev/null +++ b/library/external/dialpad/src/main/res/values-az/values-az.xml @@ -0,0 +1,8 @@ + + + "geri düyməsi" + "Geri naviqasiya edin" + "Daha çox seçim" + "plus" + "səsli məktub" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-b+sr+Latn/values-b+sr+Latn.xml b/library/external/dialpad/src/main/res/values-b+sr+Latn/values-b+sr+Latn.xml new file mode 100644 index 0000000000..4925383c9c --- /dev/null +++ b/library/external/dialpad/src/main/res/values-b+sr+Latn/values-b+sr+Latn.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Idite nazad" + "Još opcija" + "plus" + "govorna pošta" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-be/values-be.xml b/library/external/dialpad/src/main/res/values-be/values-be.xml new file mode 100644 index 0000000000..3b795ba0f6 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-be/values-be.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Перайсці назад" + "Дадатковыя параметры" + "плюс" + "галасавая пошта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-bg/values-bg.xml b/library/external/dialpad/src/main/res/values-bg/values-bg.xml new file mode 100644 index 0000000000..aa464b870b --- /dev/null +++ b/library/external/dialpad/src/main/res/values-bg/values-bg.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Преминаване назад" + "Още опции" + "плюс" + "гласова поща" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-bn/values-bn.xml b/library/external/dialpad/src/main/res/values-bn/values-bn.xml new file mode 100644 index 0000000000..78efb7eb12 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-bn/values-bn.xml @@ -0,0 +1,8 @@ + + + "ব্যাক-স্পেস" + "পিছনে যান" + "আরো বিকল্প" + "যোগ চিহ্ন" + "ভয়েসমেল" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-bs/values-bs.xml b/library/external/dialpad/src/main/res/values-bs/values-bs.xml new file mode 100644 index 0000000000..6782138188 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-bs/values-bs.xml @@ -0,0 +1,8 @@ + + + "tipka za brisanje" + "Vrati se nazad" + "Više opcija" + "plus" + "govorna pošta" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ca/values-ca.xml b/library/external/dialpad/src/main/res/values-ca/values-ca.xml new file mode 100644 index 0000000000..b4c70bdc49 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ca/values-ca.xml @@ -0,0 +1,8 @@ + + + "retrocés" + "Torna enrere" + "Més opcions" + "més" + "missatge de veu" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-cs/values-cs.xml b/library/external/dialpad/src/main/res/values-cs/values-cs.xml new file mode 100644 index 0000000000..b9f770215c --- /dev/null +++ b/library/external/dialpad/src/main/res/values-cs/values-cs.xml @@ -0,0 +1,8 @@ + + + "Backspace" + "Přejít zpět" + "Více možností" + "plus" + "hlasová zpráva" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-da/values-da.xml b/library/external/dialpad/src/main/res/values-da/values-da.xml new file mode 100644 index 0000000000..b842e27303 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-da/values-da.xml @@ -0,0 +1,8 @@ + + + "tilbagetast" + "Naviger tilbage" + "Flere valgmuligheder" + "plus" + "telefonsvarer" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-de/values-de.xml b/library/external/dialpad/src/main/res/values-de/values-de.xml new file mode 100644 index 0000000000..7d3f7b9a77 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-de/values-de.xml @@ -0,0 +1,8 @@ + + + "Rücktaste" + "Zurück" + "Mehr Optionen" + "Plus" + "Mailboxnachricht" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-el/values-el.xml b/library/external/dialpad/src/main/res/values-el/values-el.xml new file mode 100644 index 0000000000..1998e9e464 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-el/values-el.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Μετάβαση πίσω" + "Περισσότερες επιλογές" + "συν" + "αυτόματος τηλεφωνητής" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-en-rAU/values-en-rAU.xml b/library/external/dialpad/src/main/res/values-en-rAU/values-en-rAU.xml new file mode 100644 index 0000000000..5281387a9d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-en-rAU/values-en-rAU.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Navigate back" + "More options" + "plus" + "voicemail" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-en-rGB/values-en-rGB.xml b/library/external/dialpad/src/main/res/values-en-rGB/values-en-rGB.xml new file mode 100644 index 0000000000..5281387a9d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-en-rGB/values-en-rGB.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Navigate back" + "More options" + "plus" + "voicemail" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-en-rIN/values-en-rIN.xml b/library/external/dialpad/src/main/res/values-en-rIN/values-en-rIN.xml new file mode 100644 index 0000000000..5281387a9d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-en-rIN/values-en-rIN.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Navigate back" + "More options" + "plus" + "voicemail" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-es-rUS/values-es-rUS.xml b/library/external/dialpad/src/main/res/values-es-rUS/values-es-rUS.xml new file mode 100644 index 0000000000..0eda697d16 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-es-rUS/values-es-rUS.xml @@ -0,0 +1,8 @@ + + + "retroceso" + "Volver" + "Más opciones" + "más" + "buzón de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-es/values-es.xml b/library/external/dialpad/src/main/res/values-es/values-es.xml new file mode 100644 index 0000000000..3386a4e7e9 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-es/values-es.xml @@ -0,0 +1,8 @@ + + + "retroceso" + "Volver" + "Más opciones" + "más" + "mensaje de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-et/values-et.xml b/library/external/dialpad/src/main/res/values-et/values-et.xml new file mode 100644 index 0000000000..05499ef49f --- /dev/null +++ b/library/external/dialpad/src/main/res/values-et/values-et.xml @@ -0,0 +1,8 @@ + + + "tagasilüke" + "Tagasi navigeerimine" + "Rohkem valikuid" + "pluss" + "kõnepostisõnum" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-eu/values-eu.xml b/library/external/dialpad/src/main/res/values-eu/values-eu.xml new file mode 100644 index 0000000000..788a168eb6 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-eu/values-eu.xml @@ -0,0 +1,8 @@ + + + "atzera tekla" + "Egin atzera" + "Aukera gehiago" + "gehi" + "erantzungailua" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-fa/values-fa.xml b/library/external/dialpad/src/main/res/values-fa/values-fa.xml new file mode 100644 index 0000000000..e28807f063 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-fa/values-fa.xml @@ -0,0 +1,8 @@ + + + "برگشت به عقب" + "پیمایش به عقب" + "گزینه‌های بیشتر" + "به‌علاوه" + "پست صوتی" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-fi/values-fi.xml b/library/external/dialpad/src/main/res/values-fi/values-fi.xml new file mode 100644 index 0000000000..30ec4d5c8c --- /dev/null +++ b/library/external/dialpad/src/main/res/values-fi/values-fi.xml @@ -0,0 +1,8 @@ + + + "askelpalautin" + "Siirry takaisin" + "Lisää vaihtoehtoja" + "plus" + "ääniviesti" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-fr-rCA/values-fr-rCA.xml b/library/external/dialpad/src/main/res/values-fr-rCA/values-fr-rCA.xml new file mode 100644 index 0000000000..e2dc9e4527 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-fr-rCA/values-fr-rCA.xml @@ -0,0 +1,8 @@ + + + "retour arrière" + "Naviguer vers l\'arrière" + "Plus d\'options" + "plus" + "messagerie vocale" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-fr/values-fr.xml b/library/external/dialpad/src/main/res/values-fr/values-fr.xml new file mode 100644 index 0000000000..265b089baf --- /dev/null +++ b/library/external/dialpad/src/main/res/values-fr/values-fr.xml @@ -0,0 +1,8 @@ + + + "retour arrière" + "Revenir en arrière" + "Plus d\'options" + "plus" + "message vocal" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-gl/values-gl.xml b/library/external/dialpad/src/main/res/values-gl/values-gl.xml new file mode 100644 index 0000000000..932feba9df --- /dev/null +++ b/library/external/dialpad/src/main/res/values-gl/values-gl.xml @@ -0,0 +1,8 @@ + + + "retroceso" + "Volver á vista anterior" + "Máis opcións" + "máis" + "correo de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-gu/values-gu.xml b/library/external/dialpad/src/main/res/values-gu/values-gu.xml new file mode 100644 index 0000000000..78997a8be6 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-gu/values-gu.xml @@ -0,0 +1,8 @@ + + + "backspace" + "પાછળ નૅવિગેટ કરો" + "વધુ વિકલ્પો" + "પ્લસ" + "વૉઇસમેઇલ" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-hi/values-hi.xml b/library/external/dialpad/src/main/res/values-hi/values-hi.xml new file mode 100644 index 0000000000..cc10b134ee --- /dev/null +++ b/library/external/dialpad/src/main/res/values-hi/values-hi.xml @@ -0,0 +1,8 @@ + + + "backspace" + "वापस नेविगेट करें" + "अधिक विकल्प" + "धन का चिह्न" + "वॉइसमेल" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-hr/values-hr.xml b/library/external/dialpad/src/main/res/values-hr/values-hr.xml new file mode 100644 index 0000000000..19f1922c5f --- /dev/null +++ b/library/external/dialpad/src/main/res/values-hr/values-hr.xml @@ -0,0 +1,8 @@ + + + "povratna tipka" + "Kretanje natrag" + "Više opcija" + "plus" + "govorna pošta" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-hu/values-hu.xml b/library/external/dialpad/src/main/res/values-hu/values-hu.xml new file mode 100644 index 0000000000..4aa7a3943d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-hu/values-hu.xml @@ -0,0 +1,8 @@ + + + "Backspace" + "Vissza" + "További beállítások" + "plusz" + "hangposta" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-hy/values-hy.xml b/library/external/dialpad/src/main/res/values-hy/values-hy.xml new file mode 100644 index 0000000000..0c13c0e156 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-hy/values-hy.xml @@ -0,0 +1,8 @@ + + + "հետշարժ" + "Հետ գնալ" + "Այլ ընտրանքներ" + "գումարում" + "ձայնային փոստ" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-in/values-in.xml b/library/external/dialpad/src/main/res/values-in/values-in.xml new file mode 100644 index 0000000000..9384f9c220 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-in/values-in.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Tombol kembali" + "Opsi lainnya" + "tambah" + "pesan suara" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-is/values-is.xml b/library/external/dialpad/src/main/res/values-is/values-is.xml new file mode 100644 index 0000000000..bbf02c8e91 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-is/values-is.xml @@ -0,0 +1,8 @@ + + + "bakklykill" + "Fara til baka" + "Fleiri valkostir" + "plús" + "talhólfsskilaboð" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-it/values-it.xml b/library/external/dialpad/src/main/res/values-it/values-it.xml new file mode 100644 index 0000000000..563975ce15 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-it/values-it.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Torna indietro" + "Altre opzioni" + "più" + "messaggio vocale" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-iw/values-iw.xml b/library/external/dialpad/src/main/res/values-iw/values-iw.xml new file mode 100644 index 0000000000..34493a8ac6 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-iw/values-iw.xml @@ -0,0 +1,8 @@ + + + "Backspace" + "ניווט חזרה" + "אפשרויות נוספות" + "פלוס" + "דואר קולי" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ja/values-ja.xml b/library/external/dialpad/src/main/res/values-ja/values-ja.xml new file mode 100644 index 0000000000..b8c17100c2 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ja/values-ja.xml @@ -0,0 +1,8 @@ + + + "Backspace" + "戻る" + "その他のオプション" + "足す" + "ボイスメール" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ka/values-ka.xml b/library/external/dialpad/src/main/res/values-ka/values-ka.xml new file mode 100644 index 0000000000..2613d048a4 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ka/values-ka.xml @@ -0,0 +1,8 @@ + + + "უკუშლა" + "უკან დაბრუნება" + "სხვა პარამეტრები" + "პლუსი" + "ხმოვანი ფოსტა" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-kk/values-kk.xml b/library/external/dialpad/src/main/res/values-kk/values-kk.xml new file mode 100644 index 0000000000..65015a6e66 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-kk/values-kk.xml @@ -0,0 +1,8 @@ + + + "Backspace пернесі" + "Артқа қайту" + "Басқа опциялар" + "қосу" + "дауыстық пошта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-km/values-km.xml b/library/external/dialpad/src/main/res/values-km/values-km.xml new file mode 100644 index 0000000000..bb4754c842 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-km/values-km.xml @@ -0,0 +1,8 @@ + + + "លុប​ថយក្រោយ" + "រក​មើលថយ​ក្រោយ​វិញ" + "ជម្រើស​បន្ថែម" + "plus" + "សារ​ជា​សំឡេង" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-kn/values-kn.xml b/library/external/dialpad/src/main/res/values-kn/values-kn.xml new file mode 100644 index 0000000000..e49a0633bc --- /dev/null +++ b/library/external/dialpad/src/main/res/values-kn/values-kn.xml @@ -0,0 +1,8 @@ + + + "backspace" + "ಹಿಂದಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡು" + "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" + "ಸಂಕಲನ" + "ಧ್ವನಿಮೇಲ್" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ko/values-ko.xml b/library/external/dialpad/src/main/res/values-ko/values-ko.xml new file mode 100644 index 0000000000..f9ebed349a --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ko/values-ko.xml @@ -0,0 +1,8 @@ + + + "백스페이스" + "뒤로 이동" + "옵션 더보기" + "더하기" + "음성사서함" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ky/values-ky.xml b/library/external/dialpad/src/main/res/values-ky/values-ky.xml new file mode 100644 index 0000000000..a894cd7cf2 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ky/values-ky.xml @@ -0,0 +1,8 @@ + + + "артка карай өчүрүү" + "Артка кайтуу" + "Көбүрөөк мүмкүнчүлүктөр" + "кошуу" + "үн почтасы" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-land/values-land.xml b/library/external/dialpad/src/main/res/values-land/values-land.xml new file mode 100644 index 0000000000..364bf95f05 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-land/values-land.xml @@ -0,0 +1,25 @@ + + + 65dp + 5dp + 20sp + 3dp + 35dp + 0dp + + + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-lo/values-lo.xml b/library/external/dialpad/src/main/res/values-lo/values-lo.xml new file mode 100644 index 0000000000..e73d68cc95 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-lo/values-lo.xml @@ -0,0 +1,8 @@ + + + "ປຸ່ມ backspace" + "ນຳທາງກັບຄືນ" + "ໂຕເລືອກເພີ່ມເຕີມ" + "ບວກ" + "ຂໍ້ຄວາມສຽງ" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-lt/values-lt.xml b/library/external/dialpad/src/main/res/values-lt/values-lt.xml new file mode 100644 index 0000000000..858ca29dc9 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-lt/values-lt.xml @@ -0,0 +1,8 @@ + + + "naikinimo klavišas" + "Eiti atgal" + "Daugiau parinkčių" + "sudėties ženklas" + "balso pašto pranešimas" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-lv/values-lv.xml b/library/external/dialpad/src/main/res/values-lv/values-lv.xml new file mode 100644 index 0000000000..70a59dc3b5 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-lv/values-lv.xml @@ -0,0 +1,8 @@ + + + "atpakaļatkāpe" + "Pāriet atpakaļ" + "Vairāk opciju" + "pluszīme" + "balss pasts" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-mk/values-mk.xml b/library/external/dialpad/src/main/res/values-mk/values-mk.xml new file mode 100644 index 0000000000..0f958f19a0 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-mk/values-mk.xml @@ -0,0 +1,8 @@ + + + "избриши" + "Оди назад" + "Повеќе опции" + "плус" + "говорна пошта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ml/values-ml.xml b/library/external/dialpad/src/main/res/values-ml/values-ml.xml new file mode 100644 index 0000000000..43dbec3911 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ml/values-ml.xml @@ -0,0 +1,8 @@ + + + "ബാക്ക്‌സ്‌പെയ്‌സ്" + "തിരികെ പോകുക" + "കൂടുതൽ‍ ഓപ്‌ഷനുകള്‍" + "പ്ലസ്" + "വോയ്‌സ്‌മെയിൽ" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-mn/values-mn.xml b/library/external/dialpad/src/main/res/values-mn/values-mn.xml new file mode 100644 index 0000000000..86e965baf5 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-mn/values-mn.xml @@ -0,0 +1,8 @@ + + + "ухраах" + "Буцах" + "Нэмэлт сонголтууд" + "нэмэх" + "дуут шуудан" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-mr/values-mr.xml b/library/external/dialpad/src/main/res/values-mr/values-mr.xml new file mode 100644 index 0000000000..385a98df01 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-mr/values-mr.xml @@ -0,0 +1,8 @@ + + + "backspace" + "मागे नेव्हिगेट करा" + "अधिक पर्याय" + "अधिक" + "व्हॉइसमेल" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ms/values-ms.xml b/library/external/dialpad/src/main/res/values-ms/values-ms.xml new file mode 100644 index 0000000000..a41274606d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ms/values-ms.xml @@ -0,0 +1,8 @@ + + + "undur ruang" + "Navigasi kembali" + "Lagi pilihan" + "tambah" + "mel suara" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-my/values-my.xml b/library/external/dialpad/src/main/res/values-my/values-my.xml new file mode 100644 index 0000000000..7e1ff4231c --- /dev/null +++ b/library/external/dialpad/src/main/res/values-my/values-my.xml @@ -0,0 +1,8 @@ + + + "နောက်ပြန်ဖျက်ခလုတ်" + "အနောက်သို့ ပြန်သွားပါ" + "ပိုမိုရွေးချယ်စရာများ" + "အပေါင်း လက္ခဏာ" + "အသံမေးလ်" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-nb/values-nb.xml b/library/external/dialpad/src/main/res/values-nb/values-nb.xml new file mode 100644 index 0000000000..cd08c3b339 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-nb/values-nb.xml @@ -0,0 +1,8 @@ + + + "tilbaketast" + "Gå tilbake" + "Flere alternativer" + "pluss" + "talepost" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ne/values-ne.xml b/library/external/dialpad/src/main/res/values-ne/values-ne.xml new file mode 100644 index 0000000000..28ec947b80 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ne/values-ne.xml @@ -0,0 +1,8 @@ + + + "ब्याकस्पेस" + "पछाडि नेभिगेट गर्नुहोस्" + "थप विकल्पहरू" + "जोड" + "भ्वाइसमेल" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-nl/values-nl.xml b/library/external/dialpad/src/main/res/values-nl/values-nl.xml new file mode 100644 index 0000000000..6e3badad9d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-nl/values-nl.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Terug navigeren" + "Meer opties" + "plus" + "voicemail" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-no/values-no.xml b/library/external/dialpad/src/main/res/values-no/values-no.xml new file mode 100644 index 0000000000..cd08c3b339 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-no/values-no.xml @@ -0,0 +1,8 @@ + + + "tilbaketast" + "Gå tilbake" + "Flere alternativer" + "pluss" + "talepost" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-pa/values-pa.xml b/library/external/dialpad/src/main/res/values-pa/values-pa.xml new file mode 100644 index 0000000000..add9e7a3bd --- /dev/null +++ b/library/external/dialpad/src/main/res/values-pa/values-pa.xml @@ -0,0 +1,8 @@ + + + "ਬੈਕਸਪੇਸ" + "ਪਿੱਛੇ ਆਵਾਗੌਣ ਕਰੋ" + "ਹੋਰ ਚੋਣਾਂ" + "ਪਲਸ" + "ਵੌਇਸਮੇਲ" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-pl/values-pl.xml b/library/external/dialpad/src/main/res/values-pl/values-pl.xml new file mode 100644 index 0000000000..94c00bb06a --- /dev/null +++ b/library/external/dialpad/src/main/res/values-pl/values-pl.xml @@ -0,0 +1,8 @@ + + + "usuń" + "Wstecz" + "Więcej opcji" + "plus" + "poczta głosowa" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-pt-rBR/values-pt-rBR.xml b/library/external/dialpad/src/main/res/values-pt-rBR/values-pt-rBR.xml new file mode 100644 index 0000000000..49ae0d6428 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-pt-rBR/values-pt-rBR.xml @@ -0,0 +1,8 @@ + + + "voltar" + "Voltar" + "Mais opções" + "mais" + "correio de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-pt-rPT/values-pt-rPT.xml b/library/external/dialpad/src/main/res/values-pt-rPT/values-pt-rPT.xml new file mode 100644 index 0000000000..5d42ca8b61 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-pt-rPT/values-pt-rPT.xml @@ -0,0 +1,8 @@ + + + "retrocesso" + "Navegar para trás" + "Mais opções" + "mais" + "mensagem de correio de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-pt/values-pt.xml b/library/external/dialpad/src/main/res/values-pt/values-pt.xml new file mode 100644 index 0000000000..49ae0d6428 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-pt/values-pt.xml @@ -0,0 +1,8 @@ + + + "voltar" + "Voltar" + "Mais opções" + "mais" + "correio de voz" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ro/values-ro.xml b/library/external/dialpad/src/main/res/values-ro/values-ro.xml new file mode 100644 index 0000000000..34f7e9fe03 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ro/values-ro.xml @@ -0,0 +1,8 @@ + + + "tasta backspace" + "Navigați înapoi" + "Mai multe opțiuni" + "plus" + "mesaj vocal" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ru/values-ru.xml b/library/external/dialpad/src/main/res/values-ru/values-ru.xml new file mode 100644 index 0000000000..261fdf0ee6 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ru/values-ru.xml @@ -0,0 +1,8 @@ + + + "клавиша Backspace" + "Вернуться" + "Ещё" + "плюс" + "голосовая почта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-si/values-si.xml b/library/external/dialpad/src/main/res/values-si/values-si.xml new file mode 100644 index 0000000000..09a999100f --- /dev/null +++ b/library/external/dialpad/src/main/res/values-si/values-si.xml @@ -0,0 +1,8 @@ + + + "backspace බොත්තම" + "ආපසු සංචාලනය කරන්න" + "තවත් විකල්ප" + "ධන" + "හඬ තැපෑල" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sk/values-sk.xml b/library/external/dialpad/src/main/res/values-sk/values-sk.xml new file mode 100644 index 0000000000..6b8990a941 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sk/values-sk.xml @@ -0,0 +1,8 @@ + + + "spätné mazanie" + "Prejsť späť" + "Ďalšie možnosti" + "plus" + "hlasová správa" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sl/values-sl.xml b/library/external/dialpad/src/main/res/values-sl/values-sl.xml new file mode 100644 index 0000000000..2abde10f12 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sl/values-sl.xml @@ -0,0 +1,8 @@ + + + "vračalka" + "Pomik nazaj" + "Več možnosti" + "plus" + "sporočilo v odzivniku" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sq/values-sq.xml b/library/external/dialpad/src/main/res/values-sq/values-sq.xml new file mode 100644 index 0000000000..22400298fb --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sq/values-sq.xml @@ -0,0 +1,8 @@ + + + "kthim prapa" + "Kthehu prapa" + "Opsione të tjera" + "plus" + "postë zanore" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sr/values-sr.xml b/library/external/dialpad/src/main/res/values-sr/values-sr.xml new file mode 100644 index 0000000000..6f1beb2900 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sr/values-sr.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Идите назад" + "Још опција" + "плус" + "говорна пошта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sv/values-sv.xml b/library/external/dialpad/src/main/res/values-sv/values-sv.xml new file mode 100644 index 0000000000..25de9491a7 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sv/values-sv.xml @@ -0,0 +1,8 @@ + + + "backsteg" + "Tillbaka" + "Fler alternativ" + "plus" + "röstbrevlåda" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-sw/values-sw.xml b/library/external/dialpad/src/main/res/values-sw/values-sw.xml new file mode 100644 index 0000000000..e4273eca9a --- /dev/null +++ b/library/external/dialpad/src/main/res/values-sw/values-sw.xml @@ -0,0 +1,8 @@ + + + "nafasi ya nyuma" + "Rudi nyuma" + "Chaguo zaidi" + "jumlisha" + "ujumbe wa sauti" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ta/values-ta.xml b/library/external/dialpad/src/main/res/values-ta/values-ta.xml new file mode 100644 index 0000000000..1412f6ae92 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ta/values-ta.xml @@ -0,0 +1,8 @@ + + + "பேக்ஸ்பேஸ்" + "பின் செல்லும்" + "மேலும் விருப்பங்கள்" + "பிளஸ்" + "குரலஞ்சல்" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-te/values-te.xml b/library/external/dialpad/src/main/res/values-te/values-te.xml new file mode 100644 index 0000000000..ad60e2fd31 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-te/values-te.xml @@ -0,0 +1,8 @@ + + + "బ్యాక్‌స్పేస్" + "వెనుకకు నావిగేట్ చేస్తుంది" + "మరిన్ని ఎంపికలు" + "కూడిక" + "వాయిస్ మెయిల్" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-th/values-th.xml b/library/external/dialpad/src/main/res/values-th/values-th.xml new file mode 100644 index 0000000000..a9240af28a --- /dev/null +++ b/library/external/dialpad/src/main/res/values-th/values-th.xml @@ -0,0 +1,8 @@ + + + "ลบถอยหลัง" + "ย้อนกลับ" + "ตัวเลือกเพิ่มเติม" + "บวก" + "ข้อความเสียง" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-tl/values-tl.xml b/library/external/dialpad/src/main/res/values-tl/values-tl.xml new file mode 100644 index 0000000000..f5ec293cc3 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-tl/values-tl.xml @@ -0,0 +1,8 @@ + + + "backspace" + "Nagna-navigate pabalik" + "Higit pang mga pagpipilian" + "plus" + "voicemail" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-tr/values-tr.xml b/library/external/dialpad/src/main/res/values-tr/values-tr.xml new file mode 100644 index 0000000000..6006ed5857 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-tr/values-tr.xml @@ -0,0 +1,8 @@ + + + "geri tuşu" + "Geri dön" + "Diğer seçenekler" + "artı" + "sesli mesaj" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-uk/values-uk.xml b/library/external/dialpad/src/main/res/values-uk/values-uk.xml new file mode 100644 index 0000000000..0d235facdf --- /dev/null +++ b/library/external/dialpad/src/main/res/values-uk/values-uk.xml @@ -0,0 +1,8 @@ + + + "видалення символів перед курсором" + "Назад" + "Інші варіанти" + "плюс" + "голосова пошта" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-ur/values-ur.xml b/library/external/dialpad/src/main/res/values-ur/values-ur.xml new file mode 100644 index 0000000000..721322d66d --- /dev/null +++ b/library/external/dialpad/src/main/res/values-ur/values-ur.xml @@ -0,0 +1,8 @@ + + + "بیک اسپیس" + "پیچھے کو نیویگیٹ کریں" + "مزید اختیارات" + "جمع" + "صوتی میل" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-uz/values-uz.xml b/library/external/dialpad/src/main/res/values-uz/values-uz.xml new file mode 100644 index 0000000000..74e39230ef --- /dev/null +++ b/library/external/dialpad/src/main/res/values-uz/values-uz.xml @@ -0,0 +1,8 @@ + + + "orqaga" + "Orqaga qaytish" + "Boshqa parametrlar" + "qo‘shuv belgisi" + "ovozli pochta" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-vi/values-vi.xml b/library/external/dialpad/src/main/res/values-vi/values-vi.xml new file mode 100644 index 0000000000..cc83289497 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-vi/values-vi.xml @@ -0,0 +1,8 @@ + + + "phím lùi" + "Điều hướng trở lại" + "Tùy chọn khác" + "cộng" + "thư thoại" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-zh-rCN/values-zh-rCN.xml b/library/external/dialpad/src/main/res/values-zh-rCN/values-zh-rCN.xml new file mode 100644 index 0000000000..95bc2d1160 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-zh-rCN/values-zh-rCN.xml @@ -0,0 +1,8 @@ + + + "删除" + "返回" + "更多选项" + "加号" + "语音邮件" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-zh-rHK/values-zh-rHK.xml b/library/external/dialpad/src/main/res/values-zh-rHK/values-zh-rHK.xml new file mode 100644 index 0000000000..6631ffe940 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-zh-rHK/values-zh-rHK.xml @@ -0,0 +1,8 @@ + + + "退格鍵" + "返回" + "更多選項" + "加號" + "留言" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-zh-rTW/values-zh-rTW.xml b/library/external/dialpad/src/main/res/values-zh-rTW/values-zh-rTW.xml new file mode 100644 index 0000000000..bb486569a0 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-zh-rTW/values-zh-rTW.xml @@ -0,0 +1,8 @@ + + + "Backspace 鍵" + "返回" + "更多選項" + "加號" + "語音留言" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values-zu/values-zu.xml b/library/external/dialpad/src/main/res/values-zu/values-zu.xml new file mode 100644 index 0000000000..023f3aec62 --- /dev/null +++ b/library/external/dialpad/src/main/res/values-zu/values-zu.xml @@ -0,0 +1,8 @@ + + + "i-backspace" + "Zulazula uye emuva" + "Izinketho eziningi" + "hlanganisa" + "ivoyisimeyili" + \ No newline at end of file diff --git a/library/external/dialpad/src/main/res/values/values.xml b/library/external/dialpad/src/main/res/values/values.xml new file mode 100644 index 0000000000..71d9b27cef --- /dev/null +++ b/library/external/dialpad/src/main/res/values/values.xml @@ -0,0 +1,121 @@ + + + + #fcfcfc + #ececec + #333 + #89000000 + #10000000 + #737373 + #dadada + #919191 + + 80dp + 2dp + 3dp + 60dp + 8dp + 10dp + 16dp + 24sp + 34sp + 5dp + 100dp + 64dp + 12sp + 3dp + 36sp + 18sp + 23sp + 36sp + 8dp + 14dp + 8dp + 13dp + 2dp + 1dp + 10dp + 400 + 400 + backspace + Navigate back + More options + plus + voicemail + + + + ABC + DEF + GHI + JKL + MNO + PQRS + TUV + WXYZ + + # + + * + + + + + + + + + \ No newline at end of file diff --git a/library/diff-match-patch/.gitignore b/library/external/diff-match-patch/.gitignore similarity index 100% rename from library/diff-match-patch/.gitignore rename to library/external/diff-match-patch/.gitignore diff --git a/library/diff-match-patch/build.gradle b/library/external/diff-match-patch/build.gradle similarity index 100% rename from library/diff-match-patch/build.gradle rename to library/external/diff-match-patch/build.gradle diff --git a/library/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java b/library/external/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java similarity index 100% rename from library/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java rename to library/external/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java diff --git a/library/jsonviewer/.gitignore b/library/external/jsonviewer/.gitignore similarity index 100% rename from library/jsonviewer/.gitignore rename to library/external/jsonviewer/.gitignore diff --git a/library/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle similarity index 90% rename from library/jsonviewer/build.gradle rename to library/external/jsonviewer/build.gradle index ad472b0b54..4e8dc99654 100644 --- a/library/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -55,13 +55,14 @@ dependencies { implementation libs.airbnb.mavericks // Span utils - implementation 'me.gujun.android:span:1.7' - + implementation('me.gujun.android:span:1.7') { + exclude group: 'com.android.support', module: 'support-annotations' + } implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20220320' + testImplementation 'org.json:json:20220924' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/library/jsonviewer/src/main/AndroidManifest.xml b/library/external/jsonviewer/src/main/AndroidManifest.xml similarity index 100% rename from library/jsonviewer/src/main/AndroidManifest.xml rename to library/external/jsonviewer/src/main/AndroidManifest.xml diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt similarity index 100% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt rename to library/external/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt diff --git a/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml b/library/external/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml similarity index 100% rename from library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml rename to library/external/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml b/library/external/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml similarity index 100% rename from library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml rename to library/external/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml b/library/external/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml similarity index 100% rename from library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml rename to library/external/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml diff --git a/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml b/library/external/jsonviewer/src/main/res/layout/item_jv_base_value.xml similarity index 100% rename from library/jsonviewer/src/main/res/layout/item_jv_base_value.xml rename to library/external/jsonviewer/src/main/res/layout/item_jv_base_value.xml diff --git a/library/jsonviewer/src/main/res/values/colors.xml b/library/external/jsonviewer/src/main/res/values/colors.xml similarity index 100% rename from library/jsonviewer/src/main/res/values/colors.xml rename to library/external/jsonviewer/src/main/res/values/colors.xml diff --git a/library/jsonviewer/src/main/res/values/strings.xml b/library/external/jsonviewer/src/main/res/values/strings.xml similarity index 100% rename from library/jsonviewer/src/main/res/values/strings.xml rename to library/external/jsonviewer/src/main/res/values/strings.xml diff --git a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt b/library/external/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt similarity index 100% rename from library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt rename to library/external/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/ImageUtils.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/ImageUtils.kt index a3d69ae8cf..705223c55e 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/ImageUtils.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/ImageUtils.kt @@ -30,7 +30,15 @@ object ImageUtils { fun getBitmap(context: Context, uri: Uri): Bitmap? { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) + val source = ImageDecoder.createSource(context.contentResolver, uri) + val listener = ImageDecoder.OnHeaderDecodedListener { decoder, _, _ -> + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { + // Allocating hardware bitmap may cause a crash on framework versions prior to Android Q + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } + + ImageDecoder.decodeBitmap(source, listener) } else { context.contentResolver.openInputStream(uri)?.use { inputStream -> BitmapFactory.decodeStream(inputStream) diff --git a/library/ui-strings/build.gradle b/library/ui-strings/build.gradle index 860fc3c980..6a31f24c9b 100644 --- a/library/ui-strings/build.gradle +++ b/library/ui-strings/build.gradle @@ -20,3 +20,7 @@ android { jvmTarget = "11" } } + +tasks.withType( com.likethesalad.android.templates.common.tasks.BaseTask) { + it.outputs.cacheIf { true } +} diff --git a/library/ui-strings/src/main/res/values-ar/strings.xml b/library/ui-strings/src/main/res/values-ar/strings.xml index 073f961cb6..70b9a33ab5 100644 --- a/library/ui-strings/src/main/res/values-ar/strings.xml +++ b/library/ui-strings/src/main/res/values-ar/strings.xml @@ -320,7 +320,7 @@ السمة خطأ في فكّ التعمية اسم الجهاز - معرّف الجهاز + معرّف الجهاز مفتاح الجهاز صدّر مفاتيح الغرفة صدّر المفاتيح إلى ملف محلي diff --git a/library/ui-strings/src/main/res/values-bg/strings.xml b/library/ui-strings/src/main/res/values-bg/strings.xml index b29823040f..d3e9e599bc 100644 --- a/library/ui-strings/src/main/res/values-bg/strings.xml +++ b/library/ui-strings/src/main/res/values-bg/strings.xml @@ -396,7 +396,7 @@ Тема Грешка при разшифроване Публично име - Сесийно ID + Сесийно ID Ключ на устройство Експортирай E2E ключове за стая Експортиране на ключове за стая diff --git a/library/ui-strings/src/main/res/values-bn-rBD/strings.xml b/library/ui-strings/src/main/res/values-bn-rBD/strings.xml index 2f068f1bf8..7897da934e 100644 --- a/library/ui-strings/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui-strings/src/main/res/values-bn-rBD/strings.xml @@ -789,7 +789,7 @@ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন শেষ থেকে শেষ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন সেশানের কুঞ্জি - আইডি + আইডি সর্বজনীন নাম ডিক্রিপশন সমস্যা থিম diff --git a/library/ui-strings/src/main/res/values-bn-rIN/strings.xml b/library/ui-strings/src/main/res/values-bn-rIN/strings.xml index 828bc3bd34..56bde36977 100644 --- a/library/ui-strings/src/main/res/values-bn-rIN/strings.xml +++ b/library/ui-strings/src/main/res/values-bn-rIN/strings.xml @@ -693,7 +693,7 @@ ডিক্রিপশন সমস্যা সর্বজনীন নাম - আইডি + আইডি সেশানের কুঞ্জি শেষ থেকে শেষ রুমের কুঞ্জিগুলি এক্সপোর্ট করুন diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index 13a5b6c119..25c490807e 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -448,7 +448,7 @@ Tema Error al desxifrar Nom públic - ID de sessió + ID de sessió Clau de sessió Exporta les claus de la sala E2E Exporta les claus de la sala @@ -1470,7 +1470,7 @@ %d sessió activa %d sessions actives - Aquesta sessió és de confiança per a xats segurs ja que l\'has verificada tu: + Aquesta sessió és de confiança per a missatges segurs ja que l\'has verificada tu: Desconnecta aquesta sessió Gestió de sessions Veure totes les sessions @@ -1844,7 +1844,7 @@ Altres idiomes disponibles Idioma actual Motiu de l\'eliminació - Aquesta sessió és de confiança per a xats segurs ja que %1$s (%2$s) l\'ha verificat: + Aquesta sessió és de confiança per a missatges segurs ja que %1$s (%2$s) l\'ha verificat: Obtenint clau de corba No s\'ha pogut crear el xat. Comprova els usuaris que vols convidar i torna-ho a provar. Verifica manualment mitjançant text @@ -2225,7 +2225,7 @@ Tria on es desen les teves converses, et dona control i independència. Connectat a través de Matrix. Comunicació segura i independent que t\'ofereix el mateix nivell de privadesa que una conversa cara a cara a casa teva. Missatgeria pel teu equip. - Missatgeria segura. + Missatges segurs. Ets propietari de les teves converses. Tu tens el control. Trucada finalitzada • %1$s @@ -2602,8 +2602,8 @@ Tots els xats Preferències de disseny Explora sales - Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis. - Altres sessions + Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis. + Altres sessions Sessions Obre la llista d\'espais Crea un nou xat o sala @@ -2619,14 +2619,11 @@ Mostra totes les sessions (V2, WIP) Crea sala Inicia xat - Verifica la teva sessió actual per a missatges segurs millorats. Verificada · Última activitat %1$s No verificada · Última activitat %1$s Veure-ho tot (%1$d) - Sessió actual Veure detalls Verifica sessió - La sessió actual està llesta per la missatgeria segura. Sessió no verificada Sessió verificada Tipus de dispositiu desconegut @@ -2636,4 +2633,81 @@ Aquesta sala no s\'ha trobat. \nTorna-ho a provar més tard.%s Invitacions - + ${app_name} +\nHola, %s. + Nova visualització! + Prova-ho + Entra a espais + L\'aplicació de xats segurs tot en un. Per a equips, amics i organitzacions. Crea un xat o uneix-te a una sala existent per començar. + + Pensa en tancar sessió de les sessions antigues (%1$d dia o més) que ja no utilitzis. + Pensa en tancar sessió de les sessions antigues (%1$d dies o més) que ja no utilitzis. + + Prem la part superior dreta per veure l\'opció d\'enviar comentaris. + Envia comentaris + Aquí es mostraran els teus missatges no llegits, quan en tinguis. + Sense novetats. + Verifica les sessions o tanca\'n la sessió si no estan verificades. + Per simplificar ${app_name}, les pestanyes ara son opcionals. Gestiona-les mitjançant el menú de la part superior dreta. + %s +\nsembla una mica buit. + Sessions inactives + Sessions no verificades + Millora la seguretat del teu compte seguint aquestes recomanacions. + Recomanacions de seguretat + + Actiu fa %1$d dia (%2$s) + Actiu fa més de %1$d dies (%2$s) + + Aquí és on apareixeran les teves sol·licituds i invitacions. + Res de nou. + Accedeix als teus espais (part inferior dreta) més ràpid i fàcilment. + Els espais són una nova manera d\'agrupar sales i gent. Afegeix una sala o crea\'n una de nova mitjançant el botó de la part inferior dreta. + Els espais són una nova manera d\'agrupar sales i gent. Crea\'n un per començar. + Cap espai, encara. + Amaga els continguts de %s + Mostra el contingut de %s + Canvia espai + Verifica les teves sessions per obtenir missatges segurs millorats o tanca les sessions que no reconeguis o ja no utilitzis. + No llest per a missatges segurs + Llest per a missatges segurs + Aquesta sessió està llesta per a missatges segurs. + La teva sessió actual està llesta per a missatges segurs. + Verifica la teva sessió actual obtenir missatges segurs millorats. + Crea missatge directe només al primer missatge + Activa missatges directes programats + Verifica o tanca aquesta sessió per estar més segur. + Per estar més segur, tanca qualsevol sessió que no reconeguis o ja no utilitzis. + No s\'han trobat sessions inactives. + No s\'han trobat sessions no verificades. + No s\'han trobat sessions verificades. + Detalls de sessió + Esborra filtre + Última activitat + Nom de la sessió + Informació d\'aplicació, dispositiu i activitat. + Adreça IP + + Pensa en tancar sessió de les sessions antigues (%1$d dia o més) que ja no utilitzis. + Pensa en tancar sessió de les sessions antigues (%1$d dies o més) que ja no utilitzis. + + Inactiu + No verificat + Verificat + Filtra + + Inactiu durant %1$d dia o més + Inactiu durant %1$d dies o més + + Inactiu + No verificat + Verificat + Totes les sessions + Filtre + Última activitat %1$s + Dispositiu + Sessió + Sessió actual + Element simplificat amb pestanyes opcionals + Activa la nova visualització + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index b7bfeac444..1983036271 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -635,7 +635,7 @@ Motiv vzhledu Chyba dešifrování Veřejné jméno - ID relace + ID relace Klíč relace Export E2E klíčů místností Export klíčů místností @@ -2651,8 +2651,8 @@ Otevřít nastavení Všechny konverzace Zobrazit všechny relace (V2, WIP) - V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte. - Ostatní relace + V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte. + Ostatní relace Relace Seznam otevřených prostorů Vytvořit novou konverzaci nebo místnost @@ -2672,11 +2672,8 @@ Neověřeno · Poslední aktivita %1$s Ověřeno · Poslední aktivita %1$s Zobrazit všechny (%1$d) - Aktuální relace Zobrazit podrobnosti Ověřit relaci - Ověřte svou aktuální relaci pro vylepšené zabezpečené zasílání zpráv. - Vaše aktuální relace je připravena pro bezpečné zasílání zpráv. Neověřená relace Ověřená relace Neznámý typ zařízení @@ -2686,4 +2683,85 @@ Je nám líto, tato místnost nebyla nalezena. \nZkuste to prosím později.%s Pozvánky - + Vyzkoušejte to + Klepnutím vpravo nahoře zobrazíte možnost zpětné vazby. + Poskytněte zpětnou vazbu + Přístup k vašim prostorům (vpravo dole) je rychlejší a snazší než kdykoli předtím. + Přístup do prostorů + Pro zjednodušení aplikace ${app_name} jsou nyní karty nepovinné. Spravujte je pomocí nabídky vpravo nahoře. + Vítejte v novém zobrazení! + Zde se zobrazí nepřečtené zprávy, pokud nějaké máte. + Nic k nahlášení. + Univerzální zabezpečená chatovací aplikace pro týmy, přátele a organizace. Vytvořte si chat nebo se připojte k existující místnosti a začněte. + Vítejte v aplikaci ${app_name}, +\n%s. + Prostory představují nový způsob seskupování místností a osob. Pomocí tlačítka vpravo dole můžete přidat stávající místnost nebo vytvořit novou. + %s +\nvypadá trochu prázdně. + + Zvažte odhlášení ze starých relací (%1$d den nebo více), které již nepoužíváte. + Zvažte odhlášení ze starých relací (%1$d dny nebo více), které již nepoužíváte. + Zvažte odhlášení ze starých relací (%1$d dnů nebo více), které již nepoužíváte. + + Neaktivní relace + Ověřte nebo se odhlaste z neověřených relací. + Neověřené relace + Zlepšete zabezpečení svého účtu dodržováním těchto doporučení. + Bezpečnostní doporučení + + Neaktivní po dobu %1$d+ dne (%2$s) + Neaktivní po dobu %1$d+ dnů (%2$s) + Neaktivní po dobu %1$d+ dnů (%2$s) + + Zde se budou nacházet vaše nové žádosti a pozvánky. + Nic nového. + Prostory představují nový způsob seskupování místností a osob. Vytvořte si prostor a začněte. + Zatím žádné prostory. + Sbalit podprostory %s + Rozbalit podprostory %s + Změnit prostor + IP adresa + Poslední aktivita + Název relace + Informace o aplikacích, zařízeních a aktivitách. + Podrobnosti o relaci + Vyčistit filtr + Nebyly nalezeny žádné neaktivní relace. + Nebyly nalezeny žádné neověřené relace. + Nebyly nalezeny žádné ověřené relace. + + Zvažte odhlášení ze starých relací (%1$d den nebo více), které již nepoužíváte. + Zvažte odhlášení ze starých relací (%1$d dny nebo více), které již nepoužíváte. + Zvažte odhlášení ze starých relací (%1$d dnů nebo více), které již nepoužíváte. + + Neaktivní + Ověřte své relace pro vylepšené bezpečné zasílání zpráv nebo se odhlaste z těch, které již nepoznáváte nebo nepoužíváte. + Neověřeno + Pro nejlepší zabezpečení se odhlaste z každé relace, kterou již nepoznáváte nebo nepoužíváte. + Ověřeno + Filtr + + Neaktivní po dobu %1$d dne nebo déle + Neaktivní po dobu %1$d dnů nebo déle + Neaktivní po dobu %1$d dnů nebo déle + + Neaktivní + Není připraveno na bezpečné zasílání zpráv + Neověřeno + Připraveno na bezpečné zasílání zpráv + Ověřeno + Všechny relace + Filtr + Poslední aktivita %1$s + Zařízení + Relace + Aktuální relace + Pro nejlepší zabezpečení a spolehlivost tuto relaci ověřte nebo se z ní odhlaste. + Ověřte svou aktuální relaci pro vylepšené bezpečené zasílání zpráv. + Tato relace je připravena pro bezpečné zasílání zpráv. + Vaše aktuální relace je připravena pro bezpečné zasílání zpráv. + Vytvořit přímou zprávu pouze při první zprávě + Povolit odložené přímé zprávy + Zjednodušený Element s volitelnými kartami + Povolit nový vzhled + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 8e502a6392..27f46160bc 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -20,7 +20,7 @@ %s hat einen Sprachanruf getätigt. %s hat den Anruf angenommen. %s hat den Anruf beendet. - %1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s + %1$s hat den zukünftigen Nachrichtenverlauf sichtbar gemacht für %2$s alle Mitglieder, ab Einladung. alle Mitglieder, ab Beitritt. alle Mitglieder. @@ -103,7 +103,7 @@ Du hast das Bild des Raumes geändert Du hast den Raumnamen zu %1$s geändert Du hast einen Videoanruf gestartet. - Du hast einen Audioanruf gestartet. + Du hast einen Sprachanruf gestartet. Du hast den Anruf angenommen. Du hast den Anruf beendet. Du hast den zukünftigen Nachrichtenverlauf für %1$s sichtbar gemacht @@ -265,11 +265,11 @@ Räume Logdateien übermitteln Absturzberichte übermitteln - Screenshot übermitteln + Bildschirmfoto übermitteln Problem melden Bitte beschreibe das Problem. Was hast du genau gemacht\? Was sollte passieren\? Was ist tatsächlich passiert\? Problembeschreibung - Um Probleme diagnostizieren zu können, werden Protokolle des Clients zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, wie die Protokolle und der Screenshot, nicht öffentlich sichtbar sein. Wenn du nur den oben eingegebenen Text senden möchtest, die nachfolgenden Haken entsprechend entfernen: + Um Probleme diagnostizieren zu können, werden Protokolle der Anwendung zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, wie die Protokolle und das Bildschirmfoto, nicht öffentlich sichtbar sein. Wenn du nur den oben eingegebenen Text senden möchtest, die nachfolgenden Haken entsprechend entfernen: Du scheinst dein Telefon frustriert zu schütteln. Möchtest du das Fenster zum Senden eines Fehlerberichts öffnen\? Dein Fehlerbericht wurde erfolgreich übermittelt Der Fehlerbericht konnte nicht übermittelt werden (%s) @@ -278,7 +278,7 @@ Raum betreten Benutzername Abmelden - Heimserver-Adresse + Heim-Server-Adresse Suchen Sprachanruf starten Videoanruf starten @@ -308,7 +308,7 @@ Die Gegenseite hat den Anruf nicht angenommen. Information ${app_name} benötigt die Berechtigung, auf dein Mikrofon zugreifen zu können, um (Sprach-)Anrufe tätigen zu können. - ${app_name} benötigt die Berechtigung, auf Kamera und Mikrofon zu zugreifen, um Video-Anrufe durchzuführen. + ${app_name} benötigt die Berechtigung, auf Kamera und Mikrofon zuzugreifen, um Videoanrufe durchzuführen. \n \nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen. Ja @@ -351,12 +351,12 @@ Anzeigename E-Mail-Adresse hinzufügen Telefonnummer hinzufügen - Appinfo in den Systemeinstellungen öffnen. - App-Info + Anwendungsinformationen in den Systemeinstellungen anzeigen. + Anwendungsinformationen Benachrichtigungen für diesen Account Benachrichtigungen für diese Sitzung Direktnachrichten - Gruppenchats + Gruppenunterhaltungen Einladungen Anrufe Nachrichten von Bots @@ -366,7 +366,7 @@ Version OLM-Version Nutzungsbedingungen - Nutzungshinweise von Drittanbietern + Drittanbieter-Lizenzen Urheberrechtserklärung Datenschutzerklärung Cache leeren @@ -390,8 +390,8 @@ %1$s @ %2$s Authentifizierung Angemeldet als - Heimserver - Identitätsserver + Heim-Server + Identitäts-Server Bitte prüfe deinen E-Mail-Posteingang und klicke auf den in der E-Mail enthaltenen Link. Klicke anschließend auf Fortsetzen. Diese E-Mail-Adresse wird bereits verwendet. Diese Telefonnummer wird bereits verwendet. @@ -403,8 +403,8 @@ Alle Nachrichten von %s anzeigen\? Wähle ein Land Thema - Lesbarkeit des Chatverlaufs - Wer kann den Chatverlauf lesen? + Lesbarkeit des Verlaufs + Wer kann den Verlauf lesen\? Alle Nur Mitglieder Nur Mitglieder (ab Einladung) @@ -412,13 +412,13 @@ Verbannte Benutzer Erweitert Interne ID dieses Raumes - Experimentelle Einstellungen - Dies sind experimentelle Funktionen, die in unerwarteter Weise Fehler verursachen können. Mit Vorsicht zu verwenden. + Labor + Dies sind experimentelle Funktionen, die in unerwarteter Weise Fehler verursachen können. Verwende sie mit Vorsicht. Als Hauptadresse setzen Als Hauptadresse aufheben Entschlüsselungsfehler Öffentlicher Name - Sitzungs-ID + Sitzungs-ID Sitzungsschlüssel Ende-zu-Ende-Raumschlüssel exportieren Raumschlüssel exportieren @@ -447,7 +447,7 @@ Starte beim Systemstart Medien-Cache leeren Medien behalten - Für alle Nachrichten Zeitstempel anzeigen + Zeitstempel für alle Nachrichten 3 Tage 1 Woche 1 Monat @@ -500,7 +500,7 @@ Sicher, dass du einen Videoanruf starten möchtest\? Die Verbannung einer Person entfernt sie aus diesem Raum und hindert sie am erneuten Beitritt. Alle Nachrichten - URL-Vorschau im Chat + URL-Vorschau Vibriere beim Erwähnen eines Nutzers Erstellen Startseite @@ -551,9 +551,9 @@ Um %1$s weiter zu verwenden, musst die Geschäftsbedingungen begutachten und ihnen zustimmen. Jetzt prüfen Konto deaktivieren - Dies wird dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird denselben Nutzernamen erneut registrieren können. Du verlässt automatisch alle Räume, in denen du bist, und deine Kontoangaben werden vom Identitätsserver gelöscht. Diese Aktion ist unumkehrbar. + Dies wird dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird denselben Nutzernamen erneut registrieren können. Du verlässt automatisch alle Räume, in denen du bist, und deine Kontoangaben werden vom Identitäts-Server gelöscht. Diese Aktion ist unumkehrbar. \n -\nDie Deaktivierung deines Konto wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die Option unten. +\nDie Deaktivierung deines Kontos wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die Option unten. \n \nDie Sichtbarkeit deiner Nachrichten ist ähnlich wie bei E-Mails: Wenn deine Nachrichten gelöscht werden, bedeutet dies, dass von dir verschickte Nachrichten nicht mit neuen oder unregistrierten Nutzer geteilt werden. Aber registrierte Nutzer, die bereits Zugang zu diesen Nachrichten haben, behalten weiterhin Zugriff auf ihre Kopie. Bitte alle Nachrichten, die ich gesendet habe, löschen, wenn mein Konto deaktiviert wird (Warnung: Unterhaltungen werden für zukünftige Nutzer unvollständig erscheinen) @@ -605,7 +605,7 @@ %1$s: %2$s +%d Aus Unterhaltung entfernen - Linkvorschau im Chat aktivieren, falls dein Homeserver diese Funktion unterstützt. + Link-Vorschau im Chat aktivieren, falls dein Heim-Server diese Funktion unterstützt. Schreibbenachrichtigungen senden Lasse andere Benutzer wissen, dass du tippst. Markdown-Formatierung @@ -614,7 +614,7 @@ Klicke auf die Lesebestätigungen für eine detailliertere Liste. Einladungen, Entfernungen und Verbannungen bleiben sichtbar. Passwort - Starte die System-Kamera anstelle der angepassten Kamera. + Starte die Kamera des Systems anstelle der selbstdefinierten. Das Kommando \"%s\" braucht mehr Parameter oder einige Parameter sind inkorrekt. Markdown wurde aktiviert. Markdown wurde deaktiviert. @@ -729,10 +729,10 @@ Wiederherstellungsschlüssel aus Passphrase generieren. Dies kann mehrere Sekunden brauchen. Du verlierst möglicherweise den Zugang zu deinen Nachrichten, wenn du dich abmeldest oder das Gerät verlierst. Rufe Backup-Version ab… - Nutze deine Wiederherstellungspassphrase, um deinen verschlüsselten Chatverlauf lesen zu können + Nutze deine Wiederherstellungs-Passphrase, um deinen verschlüsselten Nachrichtenverlauf lesen zu können nutze deinen Wiederherstellungsschlüssel Wenn du deine Wiederherstellungspassphrase nicht weist, kannst du %s. - Nutze deinen Wiederherstellungsschlüssel, um deinen verschlüsselten Chatverlauf lesen zu können + Nutze deinen Wiederherstellungsschlüssel, um deinen verschlüsselten Nachrichtenverlauf lesen zu können Hast du deinen Wiederherstellungsschlüssel verloren\? Du kannst einen neuen in den Einstellungen einrichten. Sicherung konnte mit dieser Passphrase nicht entschlüsselt werden. Bitte stelle sicher, dass du die korrekte Wiederherstellungspassphrase eingegeben hast. Gib deinen Wiederherstellungsschlüssel ein @@ -757,7 +757,7 @@ Die Sicherung hat eine ungültige Signatur von der verifizierten Sitzung %s Die Sicherung hat eine ungültige Signatur von der nicht verifizierten Sitzung %s Um die Schlüsselsicherung für diese Sitzung zu verwenden, stelle sie jetzt mit deiner Passphrase oder deinem Wiederherstellungsschlüssel wieder her. - Deine gesicherten Schlüssel vom Server löschen\? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen können, um deinen verschlüsselten Chatverlauf zu lesen. + Deine gesicherten Schlüssel vom Server löschen\? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen können, um deinen verschlüsselten Nachrichtenverlauf zu lesen. Beim Abmelden gehen deine verschlüsselten Nachrichten verloren Schlüssel-Sicherung wird durchgeführt. Wenn du dich jetzt abmeldest, gehen deine verschlüsselten Nachrichten verloren. Schlüsselsicherung sollte bei allen Sitzungen aktiviert sein, um den Verlust verschlüsselter Nachrichten zu verhindern. @@ -889,7 +889,7 @@ Sonstige Hinweise Dritter Du siehst diesen Raum bereits! Allgemein - Einstellungen + Optionen Sicherheit und Privatsphäre Push-Regeln Keine Push-Regeln definiert @@ -908,7 +908,7 @@ (bearbeitet) Nachrichtenbearbeitung Keine Änderungen gefunden - Gespräche filtern… + Konversationen filtern … Sende eine neue Direktnachricht Das Raumverzeichnis anzeigen Link in die Zwischenablage kopiert @@ -934,7 +934,7 @@ Keine Hintergrundsynchronisation Auffindbarkeit Um fortzufahren, musst du die Nutzungsbedingungen akzeptieren. - Du verwendest keinen Identitätsserver + Du verwendest keinen Identitäts-Server Du versuchst anscheinend, eine Verbindung zu einem anderen Homeserver herzustellen. Möchtest du dich abmelden\? Push-Key: App-Anzeigename: @@ -942,13 +942,13 @@ Nutzungsbedingungen Für andere auffindbar sein Verwende Bots, Bridges, Widgets und Sticker-Pakete - Identitätsserver - Verbindung zum Identitätsserver trennen - Identitätsserver konfigurieren - Identitätsserver ändern + Identitäts-Server + Verbindung zum Identitäts-Server trennen + Identitäts-Server konfigurieren + Identitäts-Server ändern Auffindbare E-Mail-Adressen Erkennungsoptionen werden angezeigt, sobald du eine E-Mail hinzugefügt hast. - Gib einen neuen Identitätsserver ein + Gib eine Identitäts-Server-Adresse ein Konnte keine Verbindung zum Homeserver herstellen Dies ist keine Adresse eines Matrixservers Kann Homeserver nicht unter dieser URL erreichen. Bitte überprüfen @@ -986,15 +986,15 @@ Sitzungsname: Format: Du nutzt aktuell %1$s um vorhandene Kontakte zu finden und um von dir bekannten Kontakten gefunden zu werden. - Du benutzt aktuell keinen Identitätsserver. Um zu entdecken und um von dir bekannten Kontakten entdeckt zu werden, richte unten einen ein. + Aktuell nutzt du keinen Identitäts-Server. Richte einen ein, um andere zu finden und selbst auffindbar zu sein. Auffindbare Telefonnummern - Bitte gib die Adresse des Identitätsservers ein - Identitätsserver hat keine Nutzungsbedingungen - Der Identitätsserver den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du dem Besitzer des Dienstes vertraust + Bitte gib die Adresse des Identitäts-Servers ein + Identitäts-Server hat keine Nutzungsbedingungen + Der Identitäts-Server, den du ausgewählt hast, hat keine Nutzungsbedingungen. Fahre nur fort, wenn du den Betreibenden des Dienstes vertraust Eine Textnachricht wurde an %s gesendet. Bitte gib den Verifizierungscode ein, den sie enthält. Aktiviere ausführliche Logs. - Ausführliche Logs werden der Entwicklung der App dadurch helfen, dass mehr Informationen übertragen werden, wenn du einen Fehlerbericht sendest. Auch wenn dies aktiviert ist, werden keine Nachrichteninhalte oder andere privaten Daten aufgezeichnet. - Bitte erneut versuchen, nachdem du die Nutzungsbedingungen deines Homeservers akzeptiert hast. + Ausführliche Protokolle werden bei der Entwicklung der App helfen. Auch wenn dies aktiviert ist, werden keine Nachrichteninhalte oder andere privaten Daten aufgezeichnet. + Bitte erneut versuchen, nachdem du die Nutzungsbedingungen deines Heim-Servers akzeptiert hast. Bei Benutzung könnten Cookies gesetzt werden und es könnten Daten mit %s geteilt werden: Bei Benutzung könnten Daten mit %s geteilt werden: Optionen zum Finden werden erscheinen, sobald du eine Telefonnummer hinzugefügt hast. @@ -1004,7 +1004,7 @@ Navigationsmenü öffnen Raumerstellungsmenü öffnen Schließe das Raumerstellungsmenü… - Starte einen neuen Privatchat + Erstelle eine neue Direktnachricht Erstelle einen neuen Raum Schließe Key-Backup-Einblendung Zum Ende springen @@ -1052,7 +1052,7 @@ Halte auf einem Raum um mehr Optionen anzuzeigen %1$s hat den Raum für jeden, der den Link hat, öffentlich gemacht. Ungelesene Nachrichten - Privat oder in Gruppen mit Leuten chatten + Schreibe privat oder in Gruppen Halte Gespräche mittels Verschlüsselung privat Los geht\'s Wähle einen Server @@ -1063,9 +1063,9 @@ Andere Benutzerdefinierte und erweiterte Einstellungen Fortfahren - Eine Trennung von deinem Identitätsserver würde bedeuten, dass du weder von anderen Nutzern gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. - Du teilst deine E-Mail-Adressen oder Telefonnummern momentan auf dem Identitätsserver %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. - Stimme den Nutzungsbedingungen des Identitätsservers (%s) zu, um zu erlauben per E-Mail oder Telefonnummer gefunden zu werden. + Eine Trennung von deinem Identitäts-Server würde bedeuten, dass du weder von anderen gefunden werden, noch diese per E-Mail oder Telefonnummer einladen kannst. + Du teilst deine E-Mail-Adressen oder Telefonnummern momentan auf dem Identitäts-Server %1$s. Du wirst dich erneut mit %2$s verbinden müssen, um mit dem Teilen aufzuhören. + Stimme den Nutzungsbedingungen des Identitäts-Servers (%s) zu, um per E-Mail-Adresse oder Telefonnummer auffindbar zu sein zu können. Zu teilende Daten nicht verarbeitbar Erweitere und individualisiere dein Benutzererlebnis Mit %1$s verbinden @@ -1081,13 +1081,13 @@ Es tut uns leid. Dieser Server akzeptiert keine neuen Benutzerkonten. Die Anwendung kann kein neues Benutzerkonto auf diesem Server erstellen. \n -\nMöchtest du dich über eine Web-Anwendung anmelden\? +\nMöchtest du dich mit einer Web-Anwendung anmelden\? Diese E-Mail-Adresse ist mit keinem Benutzerkonto verknüpft. Passwort auf %1$s zurücksetzen E-Mail Neues Passwort Achtung! - Eine Änderung deines Passworts wird alle Ende-zu-Ende-Schlüssel zurücksetzen. Dein verschlüsselter Chatverlauf wird dadurch unlesbar. Richte die Schlüsselsicherung ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzt. + Eine Änderung deines Passworts wird alle Ende-zu-Ende-Schlüssel zurücksetzen. Dein verschlüsselter Verlauf wird dadurch unlesbar. Richte die Schlüsselsicherung ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzt. Fortfahren Diese E-Mail-Adresse ist mit keinem Benutzerkonto verknüpft Prüfe deinen Posteingang @@ -1126,9 +1126,9 @@ Es ist deine Konversation. Mache sie dir zu eigen. Premium-Hosting für Organisationen Gib die Adresse des Modular Element oder Servers ein, den du verwenden möchtest - Die Anwendung kann sich nicht bei diesem Homeserver anmelden. Der Homeserver unterstützt die folgenden Anmeldemöglichkeiten: %1$s. + Die Anwendung kann sich nicht bei diesem Heim-Server anmelden. Der Heim-Server unterstützt die folgenden Anmeldemöglichkeiten: %1$s. \n -\nMöchtest du dich mit einem Webclient anmelden\? +\nMöchtest du dich mit einer Web-Anwendung anmelden\? Dir wird eine Bestätigungsmail gesendet, um dein neues Passwort zu bestätigen. Weiter Du wurdest von allen Sitzungen abgemeldet und erhältst keine Push-Benachrichtigungen mehr. Um Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an. @@ -1272,21 +1272,21 @@ Vergleiche den Code mit dem Code auf dem Bildschirm deines Gegenübers. Nachrichten mit diesem Gegenüber sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden. Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an. - Cross-Signing - Cross-Signing ist aktiviert -\nPrivate Schlüssel auf dem Gerät. - Cross-Signing ist aktiviert + Quersignierung + Quersignierung ist aktiviert, +\nprivate Schlüssel auf dem Gerät. + Quersignierung ist aktiviert, \nSchlüssel sind vertrauenswürdig. \nPrivate Schlüssel sind nicht bekannt - Cross-Signing ist aktiviert + Quersignierung ist aktiviert, \nSchlüssel sind nicht vertrauenswürdig - Cross-Signing ist nicht aktiviert + Quersignierung ist nicht aktiviert Aktive Sitzungen Alle Sitzungen anzeigen Sitzungen verwalten Diese Sitzung abmelden Keine kryptografischen Informationen verfügbar - Diese Sitzung ist für sichere Nachrichtenübertragung vertrauenswürdig, da du sie überprüft hast: + Diese Sitzung ist für sichere Kommunikation vertrauenswürdig, da du sie überprüft hast: Verifiziere diese Sitzung, um sie als vertrauenswürdig zu markieren, und gewähren ihr Zugriff auf verschlüsselte Nachrichten. Wenn du dich nicht bei dieser Sitzung angemeldet hast, ist dein Konto möglicherweise gefährdet: Eine aktive Sitzung @@ -1301,10 +1301,10 @@ Sitzungen Vertraut Nicht vertraut - Diese Sitzung ist für sichere Nachrichtenübertragung vertrauenswürdig, weil %1$s (%2$s) sie verifiziert hat: + Diese Sitzung ist für sichere Kommunikation vertrauenswürdig, weil %1$s (%2$s) sie verifiziert hat: %1$s (%2$s) hat sich in einer neuen Sitzung angemeldet: Bis dieser Benutzer dieser Sitzung vertraut, werden an und von ihm gesendete Nachrichten mit Warnungen gekennzeichnet. Alternativ kannst du dies manuell überprüfen. - Initialisiere Cross-Signing + Quersignierung initialisieren Schlüssel zurücksetzen QR-Code Fast geschafft! Zeigt %s ein Häkchen\? @@ -1373,7 +1373,7 @@ Import der Schlüssel fehlgeschlagen Benachrichtigungskonfiguration Nachrichten mit \"@room\" - Verschlüsselte Gruppenchats + Verschlüsselte Gruppenunterhaltungen Sendet eine Nachricht als einfachen Text, ohne sie als Markdown zu interpretieren Inkorrekter Benutzername und/oder Passwort. Das eingegebene Passwort beginnt oder endet mit Leerzeichen, bitte kontrolliere es. Nachrichtenschlüssel @@ -1381,7 +1381,7 @@ Druck es aus und speichere es an einem sicheren Ort Kopier es in deinen persönlichen Cloud-Speicher Verschlüsselung ist nicht aktiviert - Raumupgrades + Raumaktualisierung Verschlüsselung aktiviert Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Erfahre mehr und verifiziere Benutzer in deren Profil. Die Verschlüsselung in diesem Raum wird nicht unterstützt @@ -1392,7 +1392,7 @@ Fast geschafft! Warte auf Bestätigung… Verschlüsselte Direktnachrichten Nachricht… - Verifiziere dich und andere, um eure Chats zu schützen + Verifiziere dich und andere, um eure Unterhaltungen zu schützen Gib zum Fortfahren deinen %s ein Datei benutzen Dies ist kein gültiger Wiederherstellungsschlüssel @@ -1409,15 +1409,15 @@ nutze deinen Schlüsselbackup-Wiederherstellungsschlüssel Wenn du dein Schlüsselbackup-Passwort nicht weißt, kannst du %s. Schlüsselbackup-Wiederherstellungsschlüssel - Screenshots innerhalb der Anwendung verhindern - Das Aktivieren dieser Einstellung setzt das FLAG_SECURE in allen Aktivitäten. Starte die Anwendung neu, damit die Änderung wirksam wird. + Bildschirmfotos der Anwendung verhindern + Das Aktivieren dieser Einstellung setzt FLAG_SECURE in allen Aktivitäten. Starte die Anwendung neu, damit die Änderung wirksam wird. Neues Benutzerpasswort festlegen… - Nutze die neueste Version von ${app_name} auf deinen anderen Geräten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} für Android oder einen anderen cross-signing-fähigen Matrix-Client + Nutze die neueste Version von ${app_name} auf deinen anderen Geräten, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} für Android oder eine andere Matrix-Anwendung, die Quersignierung unterstützt ${app_name} Web \n${app_name} Desktop ${app_name} iOS \n${app_name} Android - oder einen anderen cross-signing-fähigen Matrix Client + oder eine andere Matrix-Anwendung, die Quersignierung unterstützt Nutze die neueste Version von ${app_name} auf deinen anderen Geräten: Erzwingt das Verwerfen der aktuell ausgehende Gruppensitzung in einem verschlüsseltem Raum Wird nur in verschlüsselten Räumen unterstützt @@ -1461,7 +1461,7 @@ Ablehnen Erfolg Echtzeitverbindung konnte nicht hergestellt werden. -\nBitte den Administrator deines Homeservers, einen TURN-Server zu konfigurieren, dass Anrufe zuverlässig funktionieren. +\nBitte den Administrator deines Heim-Servers, einen TURN-Server zu konfigurieren, damit Anrufe zuverlässig funktionieren. Audiogerät auswählen Telefon Lautsprecher @@ -1555,22 +1555,22 @@ Andere verfügbare Sprachen Lade verfügbare Sprachen… Öffne AGBs von %s - Trenne Verbindung zu Identitätsserver %s\? - Dieser Identitätsserver ist veraltet. ${app_name} unterstützt nur API V2. + Verbindung zu Identitäts-Server %s trennen\? + Dieser Identitäts-Server ist veraltet. ${app_name} unterstützt nur API V2. Diese Operation ist nicht möglich. Der Homeserver ist veraltet. - Bitte konfiguriere zuerst einen Identitätsserver. - Bitte akzeptiere zuerst die AGB des Identitätsservers in den Einstellungen. + Bitte konfiguriere zuerst einen Identitäts-Server. + Bitte akzeptiere zuerst die AGB des Identitäts-Servers in den Einstellungen. Deiner Privatsphäre wegen unterstützt ${app_name} nur das Senden gehashter E-Mail-Adressen und Telefonnummern. Die Assoziierung ist fehlgeschlagen. Für diese Kennung gibt es aktuell keine Zuordnung. - Dein Homeserver (%1$s) schlägt %2$s als Identitätsserver vor + Dein Heim-Server (%1$s) schlägt %2$s als Identitäts-Server vor Benutze %1$s - Alternativ kannst du die URL eines beliebigen anderen Identitätsservers angeben - Gib die URL von einem Identitätsserver ein - Bestätigen - Lege Rolle fest + Alternativ kannst du die URL eines beliebigen anderen Identitäts-Servers angeben + Gib die Adresse eines Identitäts-Servers ein + Absenden + Rolle festlegen Rolle - Öffne Chat + Unterhaltung öffnen Stelle Mikrophon stumm Aktiviere Mikrophon Stoppe Kamera @@ -1704,7 +1704,7 @@ Alle Wiederherstellungsoptionen vergessen oder verloren\? Alles zurücksetzen Du bist beigetreten. %s ist beigetreten. - Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. + Nachrichten in dieser Unterhaltung sind Ende-zu-Ende-verschlüsselt. Verlassen Einstellungen Nachrichten hier sind Ende-zu-Ende-verschlüsselt. @@ -1744,13 +1744,13 @@ Direktnachricht Verlauf der Anfragen von Schlüsselfreigaben senden Keine weiteren Ergebnisse - Starte die Diskussion + Beginne eine Unterhaltung Autorisieren Meine Zustimmung widerrufen - Du hast zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzern entdeckt zu werden. + Du hast zugestimmt, E-Mail-Adressen und Telefonnummern an diesen Identitäts-Server zu übermitteln, um für andere auffindbar zu sein. E-Mails und Telefonnummern senden Vorschläge - Bekannte Nutzer + Bekannte Personen QR-Code Hinzufügen via QR-Code Gib die Erlaubnis, um auf die Kamera zu zugreifen. @@ -1774,7 +1774,7 @@ Suche nach Kontakten auf Matrix Raumbild einrichten Einverständnis wurde nicht abgegeben. - Teile diesen Code mit Leuten, damit sie ihn scannen und mit dir chatten können. + Teile diesen Code, damit andere ihn einlesen und mit dir schreiben können. Meinen Code teilen Mein Code Scanne einen QR-Code @@ -1794,7 +1794,7 @@ Manche Zeichen sind nicht zulässig Bitte gib eine Raumadresse an Diese Adresse ist bereits vergeben - Aktivieren, wenn der Raum nur von Mitgliedern deines Homeservers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden. + Aktivieren, wenn der Raum nur von Mitgliedern deines Heim-Servers zur internen Kommunikation verwendet wird. Das kann später nicht mehr geändert werden. Begrenze Zugang zu diesem Raum (für immer!) auf Mitglieder von %s %1$d von %2$d Keine Vorschau für diesen Raum verfügbar. Willst du direkt beitreten\? @@ -1845,11 +1845,11 @@ Einmalanmeldung Anmelden mit %s Registrieren mit %s - Mit %s weitermachen + Weiter mit %s Knopf zum Nachrichteneditor hinzufügen, der die Emoji-Tastatur öffnet Emoji-Tastatur anzeigen Nutze /confetti oder sende Nachrichten mit ❄️ oder 🎉 - Chateffekte + Effekte im Verlauf Thema ändern Raum aktualisieren Rollen, die zum Ändern verschiedener Teile des Raums erforderlich sind, auswählen @@ -1859,7 +1859,7 @@ Authentifizierung fehlgeschlagen Deine Anmeldeinformationen müssen für ${app_name} eingegeben werden, um diese Aktion auszuführen. Erneute Authentifizierung erforderlich - Cross-Signing konnte nicht eingerichtet werden + Quersignierung konnte nicht eingerichtet werden Nicht autorisierte, fehlende gültige Authentifizierungsdaten Nutzer Beim Weiterleiten des Anrufs ist ein Fehler aufgetreten @@ -1917,7 +1917,7 @@ %d Einträge Die Obergrenze ist nicht bekannt. - Dein Homeserver akzeptiert Anhänge (wie Dateien, Medien, etc.) mit einer Größe bis zu %s. + Dein Heim-Server akzeptiert Anhänge (wie Dateien, Medien, etc.) mit einer Größe bis zu %s. Datei-Upload-Obergrenze des Servers Serverversion Servername @@ -1960,7 +1960,7 @@ Diese werden in der Lage sein, %s zu durchsuchen Diese werden kein Teil von %s sein Tritt meinem Space %1$s %2$s bei - Mit Spaces kannst du Personen und Räume gruppieren. + Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Räume oder Spaces hinzufügen Vorübergehend überspringen Über welche Themen möchtest du dich in %s unterhalten\? @@ -1994,8 +1994,8 @@ Dein öffentlicher Space Betrete einen Space mit der angegebenen ID Beschreibung - Erzeuge Space… - Irgendetwas + Erzeuge Space … + Ohne Thema Allgemein Einen Space erstellen Nur für mich @@ -2051,7 +2051,7 @@ Privater Space Öffentlicher Space Unbekannte Person - Feedback geben + Rückmeldung geben Fehler beim Senden vom Feedback (%s) Dein Feedback wurde erfolgreich versandt. Danke! Mich bei Fragen kontaktieren @@ -2086,7 +2086,7 @@ Sprachnachricht pausieren Sprachnachricht abspielen Sprachnachricht aufnehmen - Dieser Raum verwendet die Raumversion %s, die von diesem Heimserver als instabil markiert ist. + Dieser Raum verwendet die Raumversion %s, die von diesem Heim-Server als instabil markiert ist. Du benötigst die Berechtigung, um einen Raum upzugraden Übergeordneten Space automatisch updaten Benutzer automatisch einladen @@ -2105,14 +2105,14 @@ Sprachnachricht Lege fest, wer diesen Raum finden und betreten kann. Klicke, um die Spaces zu bearbeiten - Spaces auswählen + Spaces wählen Mitglieder von %s können Räume finden, betrachten und betreten. Privat (Zutritt nur mit Einladung) Raumupgrades Nachrichten von Bots Raumeinladungen - Verschlüsselten Gruppenchats - Gruppenchats + Verschlüsselte Gruppennachrichten + Gruppennachrichten Verschlüsselten Direktnachrichten Direktnachrichten Mein Benutzername @@ -2129,7 +2129,7 @@ Verpasster Sprachanruf %d verpasste Sprachanrufe - Heimserver API URL + Heim-Server API URL Um Sprachnachrichten zu senden, erlaube bitte Zugriff aufs Mikrofon. Um fortzufahren, erlaube bitte in den Systemeinstellungen Zugriff auf die Kamera. Für diese Aktion fehlen einige Berechtigungen, bitte erlaube diese in den Systemeinstellungen. @@ -2188,7 +2188,7 @@ Hilfreiche Informationen zur Fehlersuche anzeigen Debug-Info anzeigen Das schaut nicht nach einer gültigen E-Mail-Adresse aus - Nach Name, ID oder E-Mail suchen + Mittels Name, ID oder E-Mail-Adresse suchen Neuen Space erstellen Zugriff Wer hat Zugriff\? @@ -2243,8 +2243,8 @@ Auffindungseinstellungen öffnen Sitzung abgemeldet! Raum verlassen! - Heimserver auswählen - Es konnte kein Heimserver mit der Adresse %s gefunden werden. Bitte überprüfe die Adresse oder wähle den Heimserver manuell. + Heim-Server auswählen + Es konnte kein Heim-Server mit der Adresse %s gefunden werden. Bitte überprüfe die Adresse oder wähle den Heim-Server manuell. Untergeordneten Space hinzufügen. Bist du dir wirklich sicher, dass du diese Informationen senden willst\? E-Mail-Adressen und Telefonnummern an %s senden @@ -2259,16 +2259,16 @@ \n%s kannst du alle unsere Bedingungen lesen. Stelle sicher, dass die richtigen Personen Zugriff auf %s haben. Du kannst jederzeit weitere Personen einladen. Wer ist Mitglied deines Teams\? - Der Identitätsserver gibt keine Bedingungen an - Bedingungen des Identitätsservers ausblenden - Bedingungen des Identitätsservers anzeigen + Der Identitäts-Server gibt keine Bedingungen an + Richtlinie des Identitäts-Servers ausblenden + Bedingungen des Identitäts-Servers anzeigen Systemeinstellungen Versionen Erhalte Hilfe bei der Bedienung von ${app_name} Hilfe und Unterstützung Hilfe Rechtliches - Entscheide, welche Spaces Zugriff auf den Raum haben sollen. Die Mitglieder der Spaces können diesen Räumen beitreten. + Entscheide, welche Spaces Zugriff auf den Raum haben sollen. Die Mitglieder der Spaces können diesen Räumen betreten. hier Hilf mit, ${app_name} zu verbessern Aktivieren @@ -2296,15 +2296,15 @@ Auffindbarkeit (%s) Per E-Mail einladen, finde deine Kontakte und mehr… Schließe die Konfiguration des Auffindbarkeitsdienstes ab. - Du verwendest derzeit keinen Identitätsserver. Um Teammitglieder einzuladen und für sie auffindbar zu sein, müssen du einen solchen Server konfigurieren. - Ich habe schon ein Konto - Sichere Nachrichtenübertragung. - Besitze deine Konversationen. - Um bestehende Kontakte ermitteln zu können, müsst du Kontaktinformationen (E-Mails und Telefonnummern) an Ihren Identitätsserver senden. Wir verschlüsseln deine Daten vor dem Senden, um den Datenschutz zu gewährleisten. - Deine Kontakte sind privat. Um in deinen Kontakten Benutzer erkennen zu können, benötigen wir deine Erlaubnis, Kontaktinformationen an deinen Identitätsserver zu senden. + Du verwendest derzeit keinen Identitäts-Server. Um Team-Mitglieder einzuladen und für sie auffindbar zu sein, konfiguriere zunächst einen. + Ich habe bereits ein Konto + Sichere Kommunikation. + Besitze deine Unterhaltungen. + Um bestehende Kontakte ermitteln zu können, musst du Kontaktinformationen (E-Mail-Adressen und Telefonnummern) an deinen Identitäts-Server übermitteln. Wir verschlüsseln deine Daten vor der Übermittlung, um den Datenschutz gewährleisten zu können. + Deine Kontakte sind privat. Um unter deinen Kontakten Matrix-Nutzer finden zu können, benötigen wir deine Erlaubnis, Kontaktinformationen an deinen Identitäts-Server zu übermitteln. Dieser Server stellt keine Richtlinie bereit. - Deine Identitätsserver-Richtlinie - Deine Heimserver Richtlinie + Richtlinie deines Identitäts-Servers + Richtlinie deines Heim-Servers ${app_name} Richtlinie Abstimmung erstellen Kontakte öffnen @@ -2340,10 +2340,10 @@ Umfrage bearbeiten Keine Stimmen abgegeben Konto erstellen - Nachrichtenaustausch für dein Team. + Kommunikation für dein Team. Ende-zu-Ende-verschlüsselt und ohne Telefonnummer nutzbar. Keine Werbung oder Datenerfassung. - Wähle wo deine Gespräche liegen, für Kontrolle und Unabhängigkeit. Verbunden mit Matrix. - Sichere und unabhängige Kommunikation, die für die gleiche Vertraulichkeit sorgt, wie ein Gespräch von Angesicht zu Angesicht in deinem eigenen Zuhause. + Wähle, wo deine Unterhaltungen gespeichert werden, um Kontrolle und Unabhängigkeit zu erhalten. Verbunden via Matrix. + Sichere und unabhängige Kommunikation, die für eine Vertraulichkeit sorgt, wie ein Gespräch von Angesicht zu Angesicht in deinen eigenen vier Wänden. Standort Die Verschlüsselung ist fehlerhaft konfiguriert Bitte kontaktiere einen Admin, um die Verschlüsselung zurückzusetzen. @@ -2363,10 +2363,10 @@ Communities Teams Wir helfen dir, in Verbindung zu kommen - Mit wem wirst du am meisten chatten\? + Mit wem wirst du am meisten schreiben\? Link zu Thread kopieren Threads anzeigen - Nachrichtenblasen anzeigen + Nachrichtenblasen Laden der Karte fehlgeschlagen Karte Hinweis: App wird neugestartet @@ -2401,7 +2401,7 @@ Beenden Live-Standort aktiviert Standort teilen - Standort teilen + Diesen Standort teilen Meinen Standort teilen Meinen Standort teilen Live-Standort teilen @@ -2409,19 +2409,19 @@ Threads nähern sich der Beta 🎉 Deaktivieren BETA - Feedback geben + Rückmeldung geben BETA - Threads Beta + Threads-Beta Threads Beta Bildschirm teilen - Ausprobieren + Probiere es aus Live bis %1$s Wähle Deine Benachrichtigungsmethode Vorläufige Implementierung: Standorte bleiben im Nachrichtenverlauf von Räumen erhalten Profil-Tag: h Standortfreigabe aktivieren - Bitte beachten: Dies ist eine Testfunktion mit einer vorübergehenden Implementierung. Das bedeutet, dass Du Deinen Standortverlauf nicht löschen kannst und dass fortgeschrittene Nutzer Deinen Standortverlauf auch dann noch sehen können, wenn Du Deinen Live-Standort nicht mehr mit diesem Raum teilst. + Bitte beachte: Dies ist eine experimentelle Funktion, die eine temporäre Implementierung nutzt. Das bedeutet, dass du deinen Standortverlauf nicht löschen kannst und erfahrene Nutzer ihn sehen können, selbst wenn du deinen Live-Standort nicht mehr mit diesem Raum teilst. Live-Standortfreigabe Aktuelles Gateway: %s Gateway @@ -2464,7 +2464,7 @@ %1$d Minuten %2$d Sekunden %1$s, %2$s, %3$s Die neuesten Profilinformationen (Avatar und Anzeigename) für alle Nachrichten anzeigen. - Aktuelle Benutzerinformationen anzeigen + Aktuelle Profilinformationen Sieht gut aus! einen Anzeigenamen wählen Zurück zum Home-Screen @@ -2480,11 +2480,11 @@ Präsenz Animierte Bilder in der Zeitleiste abspielen, sobald sie sichtbar sind Animierte Bilder automatisch abspielen - Das Endpunkt-Token konnte nicht auf dem Heimserver registriert werden: + Das Endpunkt-Token konnte nicht auf dem Heim-Server registriert werden: \n%1$s - Endpunkt erfolgreich beim Heimserver registriert. + Endpunkt erfolgreich beim Heim-Server registriert. Endpunkt-Registrierung - Dein Heimserver unterstützt derzeit keine Threads, daher kann diese Funktion evtl. nicht richtig funktionieren. Einige Nachrichten mit Threads sind möglicherweise nicht zuverlässig verfügbar. %sMöchtest Du Threads trotzdem aktivieren\? + Dein Heim-Server unterstützt derzeit keine Threads, daher könnte diese Funktion evtl. nicht richtig funktionieren. Einige Nachrichten mit Threads sind möglicherweise nicht zuverlässig verfügbar. %sMöchtest Du Threads trotzdem aktivieren\? Threads helfen dabei, Unterhaltungen beim Thema zu halten und leichter zu verfolgen. %sDie Aktivierung von Threads aktualisiert die App. Dies kann bei einigen Konten länger dauern. Wir nähern uns der Veröffentlichung einer öffentlichen Beta für Threads. \n @@ -2506,7 +2506,7 @@ Beschäftigt Die biometrische Authentifizierung konnte nicht aktiviert werden. Die biometrische Authentifizierung wurde deaktiviert, weil kürzlich eine neue biometrische Authentifizierungsmethode hinzugefügt wurde. Du kannst sie in den Einstellungen wieder aktivieren. - Der Heimserver akzeptiert keine Benutzernamen, die nur aus Ziffern bestehen. + Der Heim-Server akzeptiert keine Benutzernamen, die nur aus Ziffern bestehen. teilten ihren Live-Standort Schritt überspringen Speichern und fortfahren @@ -2521,13 +2521,13 @@ Profil personalisieren ${app_name} ist auch für den Arbeitsplatz geeignet. Die sichersten Organisationen der Welt vertrauen darauf. Threads sind noch in Arbeit, und es stehen neue, aufregende Funktionen an, wie z. B. verbesserte Benachrichtigungen. Wir würden uns sehr über Dein Feedback freuen! - Nachrichten in diesem Chat werden Ende-zu-Ende-verschlüsselt. + Nachrichten in dieser Unterhaltung werden Ende-zu-Ende-verschlüsselt. Bist du ein Mensch\? Bitte lies dir %ss Bedingungen und Richtlinien durch Server-Richtlinien Folge den Anweisungen, die an %s gesendet wurden E-Mail bestätigen - Ergebnisse sind nach Beenden der Abstimmung sichtbar + Ergebnisse werden nach Abschluss der Abstimmung sichtbar sein Prüfe deine E-Mails. Passwort zurücksetzen Gib mindestens 8 Zeichen ein. @@ -2550,12 +2550,12 @@ %d Nachricht gelöscht %d Nachrichten gelöscht - Keine Element Call-Berechtigungsabfragen - Bestätige automatisch Element Call-Widgets und erlaube Kamera- und Mikrofonzugriff + Keine Element-Call-Berechtigungsabfragen + Bestätige automatisch Element-Call-Widgets und erlaube Kamera- und Mikrofonzugriff Los ändern oder - Das Zuhause deiner Gespräche + Der Ort, an dem deine Gespräche stattfinden Das zukünftige Zuhause für deine Gespräche Systemstandard nutzen Automatisch festlegen @@ -2565,9 +2565,9 @@ E-Mail nicht bestätigt, prüfe deinen Posteingang Willkommen zurück! Passwort vergessen - Benutzername / E-Mail / Telefon + Nutzername / E-Mail-Adresse / Telefonnummer Erstelle dein Konto - Serveradresse + Server-URL Wie lautet die Adresse deines Servers\? Das wird eine Art Zuhause für deine Daten Wie lautet die Adresse deines Servers\? Muss 8 oder mehr Zeichen umfassen @@ -2585,17 +2585,17 @@ Raum erstellen Ungelesene Personen - Schreibe deine erste Nachricht, um %s zur Konversation einzuladen + Schreibe deine erste Nachricht, um %s zur Unterhaltung einzuladen Alle Sitzungen anzeigen (V2, in Arbeit) - Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt. - Andere Sitzungen + Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt. + Andere Sitzungen Sitzungen Space-Liste öffnen Beginne ein Gespräch oder erstelle einen Raum Favoriten Alle Karte laden nicht möglich -\nDieser Heimserver könnte für die Kartendarstellung nicht konfiguriert sein. +\nDieser Heim-Server könnte für die Kartendarstellung nicht konfiguriert sein. Einstellungen öffnen Dieser QR-Code ist fehlerhaft. Bitte versuche es mit einer anderen Methode. Du wirst deinen verschlüsselten Nachrichtenverlauf nicht abrufen können. Um neu zu beginnen, setze deine Sicherung und Verifizierungsschlüssel zurück. @@ -2619,7 +2619,94 @@ Entschuldigung, dieser Raum wurde nicht gefunden. \nBitte versuche es später erneut.%s Einladungen - Nicht verifiziert · Letzte Aktivität %1$s - Verifiziere deine aktuelle Sitzung für besonders sichere Nachrichtenübertragung. + Nicht verifiziert · Neueste Aktivität %1$s Nicht verifizierte Sitzung + Nicht verifizierte Sitzungen + Verbessere deine Kontosicherheit, indem du diese Empfehlungen beherzigst. + Sicherheitsempfehlungen + + Inaktiv seit %1$d+ Tag (%2$s) + Inaktiv seit %1$d+ Tagen (%2$s) + + Verifiziert · Neueste Aktivität %1$s + Verifizierte Sitzung + Unbekannter Gerätetyp + Nichts Neues. + Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Erstelle einen Space, um zu beginnen. + Noch keine Spaces. + Hier werden deine ungelesenen Nachrichten erscheinen, wenn du welche hast. + Es gibt nichts Neues. + Alle Unterhaltungen + Space wechseln + Unterhaltung beginnen + Filter + Filtern + Subspaces von %s schließen + Subspaces von %s erweitern + Andere können dich als %s finden + Erstelle Unterhaltungen mit der ersten Nachricht + Verzögerte Direktnachrichten + Historie anzeigen + Probiere es aus + Tippe oben rechts, um eine Rückmeldung zu senden. + Rückmeldung geben + Greife auf deine Spaces (unten rechts) schneller und einfacher denn je zu. + Auf Spaces zugreifen + Um dein ${app_name} zu vereinfachen, sind Tabs nun optional. Verwalte sie mit dem Menü oben rechts. + Willkommen in einer neuen Übersicht! + Die Komplettlösung für sichere Kommunikation unter Freunden, in Gruppen oder in Organisationen. Erstelle eine Unterhaltung oder trete einem bestehenden Raum bei, um loszulegen. + Willkommen bei ${app_name}, +\n%s. + Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Füge einen bestehenden Raum hinzu oder erstelle einen neuen mit der Schaltfläche unten rechts. + %s +\nsieht ein bisschen leer aus. + IP-Adresse + Sitzungsname + Anwendung, Gerät und Aktivitätsinformationen. + Sitzungsdetails + Filter zurücksetzen + Keine inaktiven Sitzungen gefunden. + Keine nicht verifizierten Sitzungen gefunden. + Keine verifizierten Sitzungen gefunden. + + Erwäge, dich aus alten (ein Tag oder mehr), nicht mehr verwendeten Sitzungen abzumelden. + Erwäge, dich aus alten (%1$d Tage oder mehr), nicht mehr verwendeten Sitzungen abzumelden. + + Inaktiv + Für besonders sichere Kommunikation verifiziere deine Sitzungen oder melde dich von ihnen ab, falls du sie nicht mehr identifizieren kannst. + Nicht verifiziert + Verifiziert + + Inaktiv seit %1$d Tag oder länger + Inaktiv seit %1$d Tagen oder länger + + Inaktiv + Nicht bereit für sichere Kommunikation + Nicht verifiziert + Für sichere Kommunikation bereit + Verifiziert + Alle Sitzungen + Gerät + Sitzung + Aktuelle Sitzung + + Erwäge, dich aus alten (ein Tag oder mehr), nicht mehr verwendeten Sitzungen abzumelden. + Erwäge, dich aus alten (%1$d Tage oder mehr), nicht mehr verwendeten Sitzungen abzumelden. + + Inaktive Sitzungen + Nicht verifizierte Sitzungen verifizieren oder abmelden. + Alle anzeigen (%1$d) + Sitzung verifizieren + Diese Sitzung ist für sichere Kommunikation bereit. + Desktop + Hier erscheinen deine neuen Anfragen und Einladungen. + Ein vereinfachtes Element mit optionalen Tabs + Neues Layout aktivieren + Neueste Aktivität + Neueste Aktivität %1$s + Verifiziere deine aktuelle Sitzung für besonders sichere Kommunikation. + Deine aktuelle Sitzung ist für sichere Kommunikation bereit. + Details anzeigen + Für bestmögliche Sicherheit und Zuverlässigkeit verifiziere diese Sitzungen oder melde dich von ihr ab. + Für die bestmögliche Sicherheit, melde dich von allen Sitzungen ab, die du nicht erkennst oder nicht mehr benutzt. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 092a01bff4..f4973f4b95 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -172,7 +172,7 @@ Θέμα Σφάλμα αποκρυπτογράφησης Όνομα συσκευής - Αναγνωριστικό συσκευής + Αναγνωριστικό συσκευής Εξαγωγή Εισαγωγή Επιλέξτε ένα ευρετήριο δωματίων diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml index 7e1925f708..f536ca00f9 100644 --- a/library/ui-strings/src/main/res/values-eo/strings.xml +++ b/library/ui-strings/src/main/res/values-eo/strings.xml @@ -1084,7 +1084,7 @@ Elporti ŝlosilojn de ĉambroj Elporti tutvoje ĉifrajn ŝlosilojn de ĉambroj Ŝlosilo de salutaĵo - Identigilo de salutaĵo + Identigilo de salutaĵo Publika nomo Eraris malĉifrado Haŭto diff --git a/library/ui-strings/src/main/res/values-es-rMX/strings.xml b/library/ui-strings/src/main/res/values-es-rMX/strings.xml index 0b38fa6a19..c82f9aff61 100644 --- a/library/ui-strings/src/main/res/values-es-rMX/strings.xml +++ b/library/ui-strings/src/main/res/values-es-rMX/strings.xml @@ -249,7 +249,7 @@ Desescojer como Dirección Principal Error en descifrar Nombre del dispositivo - Identificación del dispositivo + Identificación del dispositivo Clave del dispositivo Exportar claves de cifrado de extremo-a-extremo de salas Exportar claves de salas diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml index 4eec90fbd6..bc4299c1bd 100644 --- a/library/ui-strings/src/main/res/values-es/strings.xml +++ b/library/ui-strings/src/main/res/values-es/strings.xml @@ -415,7 +415,7 @@ Dejar de Establecer como dirección principal Error de descifrado Nombre público - ID de sesión + ID de sesión Clave de sesión Exportar claves de salas con cifrado Extremo-a-Extremo Exportar claves de sala @@ -2518,4 +2518,136 @@ El destino se ha registrado de forma satisfactoria al servidor doméstico. Registración de punto final Siguiente + Pruébalo + Danos tu opinión + Acceder a espacios + Para simplificar ${app_name}, las pestañas son opcionales. Gestiónalas usando el menú en la esquina superior derecha. + ¡Bienvenido a una nueva interfaz! + Nada que reportar. + Bienvenido a ${app_name}, +\n%s. + %s +\nparece un poco vacío. + Sesiones inactivas + Verifica o cierra sesión de sesiones sin verificar. + Sesiones sin verificar + Mejora la seguridad de tu cuenta siguiendo estas recomendaciones. + Consejos de seguridad + + Inactiva por %1$d+ día (%2$s) + Inactiva por %1$d+ días (%2$s) + + Sin verificar · Última actividad %1$s + Verificada · Última actividad %1$s + Ver todos (%1$d) + Ver detalles + Verificar sesión + Sesión sin verificar + Sesión verificada + Tipo de dispositivo desconocido + Escritorio + Web + Móvil + Mostrar todas las sesiones (V2, WIP) + Auto aprovar widgets de Element Call y dar permisos de cámara y micrófono + + %d mensaje borrado + %d mensajes borrados + + Ubicación en tiempo real + Compartir ubicación + Debes tener el permiso correspondiente para compartir ubicaciones en esta sala. + No tienes permiso para compartir ubicaciones + No se pudo cargar el mapa +\nEste servidor doméstico puede que no esté configurado para mostrar mapas. + Los resultados podrán verse cuando la encuesta termine + MSC3061: Compartir claves de sala para mensajes anteriores + Abrir ajustes + Envía tu primer mensaje para invitar a %s + Los mensajes en esta sala están encriptados de extremo a extremo. + Este código QR parece incorrecto. Por favor, intente verificar con otro método. + No serás capaz de acceder al historial de mensajes encriptado. Restablece tu backup de mensajes seguro y las claves de verificación para empezar de cero. + No se ha podido verificar el dispositivo + Para más seguridad, verifica tus sesiones y cierra cualquiera que no reconozcas o hayas dejado de usar. + Otras sesiones + Sesiones + No se puede abrir este enlace: las comunidades han sido reemplazadas por espacios + Usuario / Email / Teléfono + ¿Eres una persona\? + Sigue las instrucciones enviadas a %s + Restablecer contraseña + Olvidé mi contraseña + Volver a enviar correo + ¿No recibiste ningún email\? + Sigue las instrucciones enviadas a %s + Verifica tu email + Volver a enviar código + Código enviado a %s + Confirma tu número de teléfono + Cerrar sesión en todos los dispositivos + Restablecer contraseña + Asegúrate de que tiene al menos 8 caracteres. + Elige una nueva contraseña + Nueva contraseña + Comprueba tu email. + %s te enviará un enlace de verificación + Código de confirmación + Número de teléfono + %s necesita verificar tu cuenta + Escribe tu número de teléfono + Email + %s necesita verificar tu cuenta + Introduce tu email + Por favor, lee las condiciones de uso de %s + Políticas del servidor + Ponte en contacto + ¿Deseas hospedar tu propio servidor\? + URL del servidor + ¿Cuál es la dirección de tu servidor\? + ¿Cuál es la dirección de tu servidor\? Será donde se guarden todos tus datos + Selecciona un servidor + ¡Hola de nuevo! + Editar + O + Dónde se guardarán tus conversaciones + Dónde se guardarán tus conversaciones + Debe tener al menos 8 caracteres + Otros pueden buscarte como %s + Crea tu cuenta + Abrir lista de espacios + Crear una nueva conversación o sala + Ir + Actualizando tus datos… + Personas + Favoritos + Sin leer + Todo + Lo sentimos, esta sala no se ha encontrado. +\nPor favor, inténtelo de nuevo.%s + Usar ajustes por defecto del sistema + Escoger manualmente + Tamaño automático de fuente + Escoger tamaño de la fuente + + %1$s y %2$d otro + %1$s y %2$d otros + + %1$s y %2$s + Email no verificado, comprueba tu bandeja de entrada + Aquí es donde tus nuevas solicitudes y invitaciones estarán. + Nada nuevo. + Invitaciones + Los espacios son una nueva forma de agrupar salas y personas. Crea un espacio para empezar. + No hay espacios aún. + A - Z + Actividad + Ordenar por + Mostrar recientes + Mostrar filtros + Ajustes de disposición + Explorar salas + Cambiar espacio + Crear sala + Iniciar conversación + Todas las conversaciones diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 55fb9dfef0..dbdbbdbb00 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -612,7 +612,7 @@ Need on alles katsejärgus olevad funktsionaalsused. Ole kasutamisel ettevaatlik. Dekrüptimise viga Avalik nimi - Sessiooni tunnus + Sessiooni tunnus Sessiooni võti Ekspordi jututubade läbiva krüptimise võtmed Ekspordi jututoa võtmed @@ -2592,9 +2592,9 @@ Ava seadistused Kõik vestlused Näita kõiki sessioone (V2, WIP) - Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta. - Muud sessioonid - Sessionid + Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta. + Muud sessioonid + Sessioonid Ava kogukondade loend Alusta uut vestlust või loo uus jututuba Inimesed @@ -2613,11 +2613,8 @@ Verifitseerimata · Viimati kasutusel %1$s Verifitseeritud · Viimati kasutusel %1$s Näita kõiki (%1$d) - Praegune sessioon Vaata lisateavet Verifitseeri sessioon - Turvalise sõnumivahetuse nimel palun verifitseeri oma praegune sessioon. - Sinu praegune sessioon on valmis turvaliseks sõnumivahetuseks. Verifitseerimata sessioon Verifitseeritud sessioon Tundmatu seadme tüüp @@ -2627,4 +2624,81 @@ Vabandust, aga seda jututuba ei õnnestu leida. \nPalun proovi hiljem uuesti.%s Kutsed - + Uut teavet ei leidu. + Kogukonnad on viis jututubade ja inimeste ühendamiseks. Alustamiseks võid luua uue kogukonna. + Siin veel pole kogukondi. + Vaheta kogukonda + Proovi nüüd + Tagasiside valikute nägemiseks klõpsi ülal paremal. + Jaga tagasisidet + Kogukonnad leiad alt paremalt kiiremini ja lihtsamini, kui varem. + Ligipääs kogukondadele + Et ${app_name}\'i kasutamine oleks lihtsam, siis kaardid on nüüd valikulised. Neid saad hallata ülal paremal avanevast menüüst. + Meie liidesel on nüüd uus vaade! + Kui sul on lugemata sõnumeid, siis nad on siit leitavad. + Hetkel siin polegi midagi põnevat. + Paljude võimalustega turvaline suhtlusrakendus sõprade, kogukondade ja tiimide jaoks. Alustamiseks loo mõni uus vestlus või liitu olemasoleva jututoaga. + %s, +\ntere tulemast ${app_name} kasutajaks. + Kogukonnad on võimalus jututubade ja inimeste ühendamiseks. Kasutades all paremal olevat nuppu lisa mõni olemasolev jututuba või loo uus. + %s +\ntundub olema tühjavõitu. + + Logi välja sellisest vanast sessioonist (vanem kui %1$d päev), mida sa enam ei kasuta. + Logi välja sellistest vanadest sessioonidest (vanemad kui %1$d päeva), mida sa enam ei kasuta. + + Mitteaktiivsed sessioonid + Logi verifitseerimata sessioonidest välja või verifitseeri nad. + Verifitseerimata sessioonid + Kui järgid neid soovitusi, siis sa parandad oma kasutajakonto turvalisust. + Turvalisusega seotud soovitused + + Pole olnud kasutusel %1$d+ päeva (%2$s) + Pole olnud kasutusel %1$d+ päeva (%2$s) + + Siin saavad olema sinu tulevased päringud ja kutsed. + Ahenda %s alamkogukonnad + Näita %s alamkogukondi + IP-aadress + Viimati kasutusel + Sessiooni nimi + Rakendus, seade ja kasutamise teave. + Sessiooni teave + Eemalda filter + Ei leidu sessioone, mis pole aktiivses kasutuses. + Verifitseerimata sessioone ei leidu. + Verifitseeritud sessioone ei leidu. + + Kaalu vanadest ja kasutamata sessioonidest väljalogimist (vanemad kui %1$d või enam päeva). + Kaalu vanadest ja kasutamata sessioonidest väljalogimist (vanemad kui %1$d või enam päeva). + + Pole pidevas kasutuses + Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning logi neist välja, mida sa enam ei kasuta või ei tunne enam ära. + Verifitseerimata + Parima turvalisuse nimel logi välja neist sessioonidest, mida sa enam ei kasuta või ei tunne ära. + Verifitseeritud + Filtreeri + + Pole olnud kasutusel %1$d või enam päeva + Pole olnud kasutusel %1$d või enam päeva + + Pole pidevas kasutuses + Pole valmis turvaliseks sõnumivahetuseks + Verifitseerimata + Valmis turvaliseks sõnumivahetuseks + Verifitseeritud + Kõik sessioonid + Filtreeri + Viimati kasutusel %1$s + Seade + Sessioonid + Praegune sessioon + Parima turvalisuse ja töökindluse nimel verifitseeri see sessioon või logi ta võrgust välja. + Turvalise sõnumivahetuse nimel palun verifitseeri oma praegune sessioon. + See sessioon on valmis turvaliseks sõnumivahetuseks. + Sinu praegune sessioon on valmis turvaliseks sõnumivahetuseks. + Alusta otsevestlust esimese sõnumiga + Võta kasutusele viivitusega otsevestlused + Lihtsustatud Element valikuliste kaartidega + Võta kasutusele rakenduse uus välimus + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-eu/strings.xml b/library/ui-strings/src/main/res/values-eu/strings.xml index 7b27d1cc1d..f1f834ee04 100644 --- a/library/ui-strings/src/main/res/values-eu/strings.xml +++ b/library/ui-strings/src/main/res/values-eu/strings.xml @@ -406,7 +406,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Deszifratze errorea Izen publikoa - IDa + IDa Saioaren gakoa Esportatu E2E geletako gakoak diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index e104225389..9012bc2ebe 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -678,7 +678,7 @@ این‌ها ویژگی‌های آزمایشی‌ای هستند که ممکن است به روش‌های نامنتظره‌ای حراب شوندا. با احتیاط استفاده کنید. تنظیم به عنوان نشانی اصلی نام عمومی - شناسهٔ نشست + شناسهٔ نشست کلید نشست برون‌ریزی کلید‌های اتاق‌های سرتاسری برون‌ریزی کلید‌های اتاق‌ها @@ -2601,8 +2601,8 @@ گشودن تنظیمات تمامی گپ‌ها نمایش تمامی نشست‌ها (ن۲، دح‌ت) - برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید. - دیگر نشست‌ها + برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید. + دیگر نشست‌ها نشست‌ها گشودن سیاههٔ فضاها ایجاد اتاق یا گفت‌وگویی جدید @@ -2622,11 +2622,8 @@ تأیید نشده · آخرین فعّالیت %1$s تأیید شده · آخرین فعّالیت %1$s دیدن همه (%1$d) - نشست کنونی دیدن جزییات تأیید نشست - نشست کنونیتان را برای پیام‌رسانی امن بهبود یافته تأیید کنید. - نشست کنونیتان برای پیام‌رسانی امن آماده است. نشست تأیید نشده نشست تأیید شده گونهٔ افزاره ناشناخته @@ -2636,4 +2633,76 @@ متأسفانه این اتاق پیدا نشد. \nلطفاً بعداً دوباره تلاش کنید.%s دعوت‌ها - + زدن بالا سمت چپ برای دیدن گزینهٔ بازخورد. + دسترسی به فضاهایتان (پایین سمت چپ) سریع‌تر و ساده‌تر از همیشه. + برای ساده‌سازی ${app_name} زبانه‌ها اختیاری شده‌اند. مدیریت با استفاده از فهرست بالا سمت چپ. + این جایی است که پیام‌های ناخوانده‌تان در صورت وجود ظاهر خواهند شد. + کارهٔ گپ امن یکپارچه برای گروه‌ها، دوستان و سازمان‌ها. برای آغاز، گپی ساخته یا به اتاقی بپیوندید. + فضاها راهی جدید برای گروه‌بندی اتاق‌ها و افراد است. با استفاده از دکمهٔ پایین سمت چپ فضایی ساخته یا اتاقی را بیفزایید. + %s +\nکمی خالی به نظر می‌رسد. + + در نظر گرفتن خروج از نشست‌های قدیمی (۱ روز یا بیش‌تر) که دیگر استفاده نمی‌کنید. + در نظر گرفتن خروج از نشست‌های قدیمی (%1$d روز یا بیش‌تر) که دیگر استفاده نمی‌کنید. + + تأیید یا خروج از نشست‌های تأییدنشده. + بهبود امنیت حسابتان با پیروی از این توصیه‌ها. + + غیرفعّال برای بیش از %1$d روز (%2$s) + غیرفعّال برای بیش از %1$d روز (%2$s) + + این جایی است که درخواست‌ها و دعوت‌های جدیدتان خواهند بود. + فضاها راهی جدید برای گروه‌بندی اتاق‌ها و افراد است. برای آغاز، فضایی بسازید. + بیازماییدش + دادن بازخورد + دسترسی به فضاها + به نمایی جدید خوش آمدید! + چیزی برای گزارش نیست. + %s +\nبه ${app_name} خوش آمدی. + نشست‌های غیرفعّال + نشست‌های تأیید نشده + توصیه‌های امنیتی + چیز جدیدی نیست. + هنوز فضایی وجود ندارد. + جمع کردن فرزندان %s + گسترش فرزندان %s + تغییر فضا + نشانی آی‌پی + واپسین فعّالیت + نام نشست + اطّلاعات برنامه، افزاره و فعّالیت. + جزییات نشست + پاک‌سازی پالایه + هیچ نشست غیرفعّالی پیدا نشد. + هیچ نشست تأیید نشده‌ای پیدا نشد. + هیچ نشست تأیید نشده‌ای پیدا نشد. + غیرفعّال + تأیید نشده + برای بهترین امنیت، از هرنشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید، خارج شوید. + تأیید شده + پالایه + غیرفعّال + نا آماده برای پیام‌رسانی امن + تأیید نشده + آمادهٔ پیام‌رسانی امن + تأیید شده + تمامی نشست‌ها + پالایه + آخرین فعّالیت %1$s + افزاره + نشست + نشست کنونی + برای بهترین امنیت و اطمینان این نشست را تأیید کرده یا خارج شوید. + تأیید نشست کنونیتان برای پیام‌رسانی امن. + این نشست برای پیام‌رسانی امن آماده است. + نشست کنونیتان برای پیام‌رسانی امن آماده است. + ایجاد پیام خصوصی فقط در نخستین پیام + المنتی ساده شده با زبانه‌های انتخابی + به کار انداختن چینش جدید + تأیید نشست‌هایتان برای پیام‌رسانی امن بهبود یافته یا خروج از آن‌هایی که تشخیصشان نداده یا دیگر استفاده نمی‌کنید. + + غیرفعّال برای ۱ روز یا بیش‌تر + غیرفعّال برای %1$d روز یا بیش‌تر + + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fi/strings.xml b/library/ui-strings/src/main/res/values-fi/strings.xml index fde2502ae0..a576e7f0dc 100644 --- a/library/ui-strings/src/main/res/values-fi/strings.xml +++ b/library/ui-strings/src/main/res/values-fi/strings.xml @@ -366,7 +366,7 @@ Kumoa pääosoitteeksi asettaminen Salauksenpurkuvirhe Julkinen nimi - Istunnon tunnus + Istunnon tunnus Istunnon avain Vie salatun huoneen avaimet Vie huoneen avaimet diff --git a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml index 29a618f415..94db2935a7 100644 --- a/library/ui-strings/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui-strings/src/main/res/values-fr-rCA/strings.xml @@ -778,7 +778,7 @@ Exporter les clés des salons Exporter les clés E2E des salons Clé de la session - Identifiant de session + Identifiant de session Nom public Erreur de déchiffrement Thème diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index 55bd14d3a0..d3a46e03da 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -346,7 +346,7 @@ Désactiver comme adresse principale Erreur de déchiffrement Nom public - Identifiant de session + Identifiant de session Clé de la session Exporter les clés des conversations Exporter les clés des conversations @@ -2601,8 +2601,8 @@ Ouvrir les paramètres Toutes les conversations Afficher toutes les sessions (V2, en cours) - Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous n’utilisez plus. - Autres sessions + Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous n’utilisez plus. + Autres sessions Sessions Ouvrir la liste des espaces Créer une nouvelle conversation ou salon @@ -2622,11 +2622,8 @@ Non vérifiée · Dernière activité %1$s Vérifié · Dernière activité %1$s Tout voir (%1$d) - Cette session Voir les détails Vérifier la session - Vérifiez votre session pour une sécurité renforcée de votre messagerie. - Votre session est prête pour l’envoi de messages sécurisés. Session non vérifiée Session vérifiée Type de périphérique inconnu @@ -2636,4 +2633,81 @@ Désolé, impossible de trouver ce salon. \nVeuillez réessayer plus tard.%s Invitations - + Essayez + Appuyez en haut à droite pour les options des avis. + Donner mon avis + Accédez à vos espaces (en bas à droite) plus rapidement et facilement qu’avant. + Accéder aux espaces + Pour simplifier Element, les onglets sont désormais facultatifs. Gérez les depuis le menu en haut à droite. + Bienvenu dans une nouvelle vue ! + C\'est ici que vos messages non-lus s’afficheront lorsque vous en aurez. + Rien à signaler. + La messagerie sécurisée tout-en-un pour les équipes, les amis, et les organisations. Créez une discussion ou rejoignez un salon pour démarrer. + Bienvenue dans ${app_name}, +\n%s. + Les espaces sont un nouveau moyen de grouper les salons et les gens. Ajoutez un salon, ou créez en un nouveau à l’aide du bouton en bas à droite. + %s +\na l’air un peu vide. + + Pensez à vous déconnecter des anciennes sessions (%1$d jour ou plus) que vous n’utilisez plus. + Pensez à vous déconnecter des anciennes sessions (%1$d jours ou plus) que vous n’utilisez plus. + + Sessions inactives + Vérifier ou déconnecter les sessions non vérifiées. + Sessions non vérifiées + Améliorez la sécurité de votre compte à l’aide de ces recommandations. + Recommandations de sécurité + + Inactif depuis %1$d+ jour (%2$s) + Inactif depuis %1$d+ jours (%2$s) + + C’est l’endroit où se trouveront vos nouvelles requêtes et invitations. + Rien de neuf. + Les espaces sont un nouveau moyen de regrouper les salons et les gens. Créez un espace pour commencer. + Pas d’espace pour l’instant. + Réduire %s enfants + Développer %s enfants + Changer d’espace + Adresse IP + Dernière activité + Nom de la session + Application, appareil et information sur l’activité. + Détails de session + Supprimer les filtres + Aucune session inactive n’a été trouvée. + Aucune session non vérifiée n’a été trouvée. + Aucune session vérifiée n’a été trouvée. + + Pensez à vous déconnecter des anciennes sessions (%1$d jour ou plus) que vous n’utilisez plus. + Pensez à vous déconnecter des anciennes sessions (%1$d jours ou plus) que vous n’utilisez plus. + + Inactif + Vérifiez vos sessions pour améliorer la sécurité de votre messagerie, ou déconnectez celles que vous ne connaissez pas ou n’utilisez plus. + Non vérifié + Pour une meilleure sécurité, déconnectez toutes les sessions que vous ne connaissez pas ou que vous n’utilisez plus. + Vérifié + Filtrer + + Inactif depuis %1$d jour ou plus + Inactif depuis %1$d jours ou plus + + Inactif + Pas prêt pour une messagerie sécurisée + Non vérifié + Prêt pour une messagerie sécurisée + Vérifié + Toutes les sessions + Filtrer + Dernière activité %1$s + Appareil + Session + Cette session + Vérifiez ou déconnectez cette session pour une meilleure sécurité et fiabilité. + Vérifiez votre session pour une sécurité accrue de votre messagerie. + Cette session est prête pour l’envoi de messages sécurisés. + Votre session est prête pour l’envoi de messages sécurisés. + Créer la conversation seulement lors du premier message + Activer les conversations privées différées + Un Element simplifié avec des onglets optionnels + Activer la nouvelle présentation + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-gl/strings.xml b/library/ui-strings/src/main/res/values-gl/strings.xml index e6d26a63e5..c1e4e40a81 100644 --- a/library/ui-strings/src/main/res/values-gl/strings.xml +++ b/library/ui-strings/src/main/res/values-gl/strings.xml @@ -380,7 +380,7 @@ Tema Fallo ao descifrar Nome do dispositivo - ID de sesión + ID de sesión Chave do dispositivo Exportar chaves E2E da sala Exportar chaves da sala diff --git a/library/ui-strings/src/main/res/values-hr/strings.xml b/library/ui-strings/src/main/res/values-hr/strings.xml index dc5930b933..6d52e5cd96 100644 --- a/library/ui-strings/src/main/res/values-hr/strings.xml +++ b/library/ui-strings/src/main/res/values-hr/strings.xml @@ -572,7 +572,7 @@ Tema Greška u dešifriranju Javni naziv - Identitet + Identitet Ključ sesije Izvezi sobne ključeve za E2E Izvezi sobne ključeve diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index af8bf26b2e..cac0a2eb5d 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -351,7 +351,7 @@ Kiszedés fő címek közül Visszafejtés hiba Nyilvános név - Munkamenet-azonosító + Munkamenet-azonosító Munkamenet kulcs E2E szoba kulcsok exportálása Szoba kulcsok exportálása @@ -2603,11 +2603,8 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Nem ellenőrzött - Utolsó aktivitás %1$s Ellenőrzött - Utolsó tevékenység %1$s Összes megtekintése (%1$d) - Jelenlegi munkamenet Részletek megtekintése Munkamenet hitelesítése - Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. - Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. Ellenőrizetlen munkamenet Ellenőrzött munkamenet Ismeretlen eszköztípus @@ -2615,12 +2612,12 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Web Mobil Minden munkamenet megjelenítése (V2, WIP) - A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz. - Más munkamenetek + A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz. + Más munkamenetek Munkamenetek Nyitott területek listája Új beszélgetés vagy szoba létrehozása - Résztvevők + Emberek Kedvencek Olvasatlan Mind @@ -2636,4 +2633,81 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Szobák felfedezése Szoba létrehozása Chat indítása - + Próbáld ki + Visszajelzés adása + A terekhez való hozzáférés (jobbra lent) gyorsabb és egyszerűbb mint valaha. + Hozzáférés a terekhez + ${app_name} egyszerűsítéséhez a lapok mostantól választhatók. Beállítani a jobb felső menüből lehet. + Üdv az új kinézetben! + Ez az a hely ahol az olvasatlan üzeneteid megjelennek, ha lesznek. + Nincs semmi említésre méltó. + A minden-egyben biztonságos csevegő alkalmazás csapatoknak, barátoknak és szervezeteknek. Kezd egy csevegést vagy lépj be egy meglévő szobába kezdésnek. + Üdv itt: ${app_name}! +\n%s. + Szobák és emberek csoportokba rendezésének új mondja a terek használata. Létező szoba hozzáadása vagy új készítése a jobb alsó gombbal. + %s +\nkicsit üresnek tűnik. + Nem aktív munkamenetek + Ellenőrizd vagy jelentkezz ki az ellenőrizetlen munkamenetekből. + Meg nem erősített munkamenetek + Javítsa a fiókja biztonságát azzal, hogy követi a következő javaslatokat. + Biztonsági javaslatok + Semmi új. + Terekkel lehet szobákat és személyeket csoportokba rendezni. Készíts egyet indulásnak. + Nincsenek terek egyelőre. + %s összezárása + %s kinyitása + Tér cseréje + A visszajelzési lehetőségekhez koppint jobb felső sarokba. + + Fontold meg, hogy a régi már nem használt (%1$d napja vagy régebben) munkamenetből kijelentkezel. + Fontold meg, hogy a régi már nem használt (%1$d napja vagy régebben) munkamenetből kijelentkezel. + + + %1$d+ napja inaktív (%2$s) + %1$d+ napja inaktív (%2$s) + + Itt láthatók a meghívók és elvégzendő műveletek. + IP cím + Utolsó tevékenység + Munkamenet neve + Alkalmazás, eszköz és aktivitás információ. + Munkamenet információk + Szűrő törlése + Nincs inaktív munkamenet. + Nincs ellenőrizetlen munkamenet. + Nincs ellenőrzött munkamenet. + + Fontold meg, hogy kijelentkezel a régi munkamenetekből (%1$d napja vagy régebben használtál) amit már nem használsz. + Fontold meg, hogy kijelentkezel a régi munkamenetekből (%1$d napja vagy régebben használtál) amit már nem használsz. + + Inaktív + Erősítse meg a munkameneteit a még biztonságosabb csevegéshez vagy jelentkezzen ki ezekből, ha nem ismeri fel vagy már nem használja őket. + Ellenőrizetlen + A legjobb biztonság érdekében jelentkezz ki minden olyan munkamenetből amit nem ismersz fel vagy régen használtál már. + Hitelesített + Szűrés + + %1$d napja inaktív + %1$d napja inaktív + + Inaktív + Nem áll készen a biztonságos üzenetküldésre + Ellenőrizetlen + Felkészülve a biztonságos üzenetküldésre + Hitelesített + Minden munkamenet + Szűrés + Utolsó aktivitás %1$s + Eszköz + Munkamenet + Jelenlegi munkamenet + A jobb biztonság vagy megbízhatóság érdekében ellenőrizze vagy jelentkezzen ki ebből a munkamenetből. + Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. + Ez a munkamenet beállítva a biztonságos üzenetküldéshez. + Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. + Közvetlen beszélgetés indítása csak az első üzenettel + Késleltetett közvetlen üzenetek engedélyezése + Egyszerűsített Element opcionálisan lapokkal + Új kinézet engedélyezése + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index d1e68b4529..7b103a9131 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -301,7 +301,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tema Kesalahan dekripsi Nama perangkat - ID Sesi + ID Sesi Kunci perangkat Ekspor kunci ruangan terenkripsi Ekspor ruangan kunci @@ -593,7 +593,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Abaikan pengguna Turunkan Anda tidak akan dapat membatalkan perubahan ini karena Anda menurunkan diri sendiri, jika Anda adalah pengguna istimewa terakhir di ruangan itu akan tidak mungkin untuk mendapatkan kembali hak istimewa. - Turunkan dirimu\? + Turunkan diri Anda\? Batalkan undangan Ruangan ini tidak umum. Anda tidak akan dapat bergabung kembali tanpa undangan. Izinkan untuk mengakses kontak. @@ -2553,8 +2553,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Email belum diverifikasi, periksa kotak masuk Anda Semua Obrolan Tampilkan Semua Sesi (V2, Dalam Pengembangan) - Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi. - Sesi lainnya + Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi. + Sesi lainnya Sesi Buka daftar space Buat percakapan atau ruangan baru @@ -2576,11 +2576,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Belum diverifikasi · Aktivitas terakhir %1$s Terverifikasi · Aktivitas terakhir %1$s Tampilkan Semua (%1$d) - Sesi Saat Ini Tampilkan Detail Verifikasi Sesi - Verifikasi sesi Anda saat ini untuk perpesanan yang aman. - Sesi Anda saat ini siap untuk perpesanan yang aman. Sesi belum diverifikasi Sesi terverifikasi Tipe perangkat tidak diketahui @@ -2588,4 +2585,77 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Web Ponsel Undangan - + Coba + Ketuk kanan atas untuk melihat opsi untuk memberikan masukan. + Beri Masukan + Aplikasi obrolan aman untuk tim, teman, dan organisasi. Buat sebuah obrolan, atau bergabung ke ruangan yang sudah ada, untuk memulai. + Akses Space Anda (di kanan bawah) dengan lebih cepat dan lebih mudah dari sebelumnya. + Akses Space + Untuk membuat ${app_name} Anda lebih sederhana, fitur tab sekarang opsional. Kelola menggunakan menu kanan atas. + Selamat datang di tampilan yang baru! + Ini di mana pesan Anda yang belum dibaca akan ditampilkan, ketika Anda menerimanya. + Tidak ada untuk dilaporkan. + Selamat datang di ${app_name}, +\n%s. + Space adalah cara baru untuk mengelompokkan ruangan dan orang. Tambahkan ruangan yang sudah ada, atau buat yang baru, dengan tombol di kanan bawah. + %s +\nkelihatannya masih kosong. + + Pertimbangkan untuk mengeluarkan sesi lawas (%1$d hari atau lebih) yang Anda tidak gunakan lagi. + + Sesi yang tidak aktif + Verifikasi atau keluarkan sesi yang belum diverifikasi. + Sesi yang belum diverifikasi + Perbaiki keamanan akun Anda dengan mengikuti saran berikut. + Saran keamanan + + Tidak aktif selama %1$d+ hari (%2$s) + + Ini di mana permintaan dan undangan baru Anda akan berada. + Belum ada yang baru. + Space adalah cara baru untuk mengelompokkan ruangan dan orang. Buat sebuah space untuk memulai. + Belum ada space. + Tutup %s anak + Buka %s anak + Ubah Space + Alamat IP + Aktivitas terakhir + Nama sesi + Informasi aplikasi, perangkat, dan aktivitas. + Detail sesi + Hapus Saringan + Tidak ditemukan sesi yang tidak aktif. + Tidak ditemukan sesi yang belum diverifikasi. + Tidak ditemukan sesi yang terverifikasi. + + Pertimbangkan untuk mengeluarkan sesi lawas (%1$d hari atau lebih) yang Anda tidak gunakan lagi. + + Tidak aktif + Verifikasi sesi Anda untuk perpesanan aman yang terbaik atau keluarkan sesi yang Anda tidak kenal atau gunakan lagi. + Belum diverifikasi + Untuk keamanan yang terbaik, keluarkan sesi yang Anda tidak kenal atau gunakan lagi. + Terverifikasi + Saring + + Tidak aktif selama %1$d hari atau lebih + + Tidak aktif + Belum siap untuk perpesanan aman + Belum diverifikasi + Siap untuk perpesanan aman + Terverifikasi + Semua sesi + Saring + Aktivitas terakhir %1$s + Perangkat + Sesi + Sesi Saat Ini + Verifikasi atau keluarkan sesi ini untuk keamanan dan keandalan yang terbaik. + Verifikasi sesi Anda saat ini untuk perpesanan aman yang baik. + Sesi ini siap untuk perpesanan aman. + Sesi Anda saat ini siap untuk perpesanan aman. + Buat pesan langsung hanya pada pesan pertama + Aktifkan pesan langsung tangguhan + Sebuah Element yang sederhana dengan fitur tab opsional + Aktifkan tata letak baru + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-is/strings.xml b/library/ui-strings/src/main/res/values-is/strings.xml index 7818761145..69191e1741 100644 --- a/library/ui-strings/src/main/res/values-is/strings.xml +++ b/library/ui-strings/src/main/res/values-is/strings.xml @@ -193,7 +193,7 @@ Þema Afkóðunarvilla Heiti tækis - Auðkenni setu + Auðkenni setu Dulritunarlykill setu Flytja út Settu inn lykilsetningu @@ -1536,7 +1536,7 @@ Yfirfarðu þennan tengil Ef þú frumstillir allt Næstum því búið! Bíð eftir staðfestingu… - Bæta við umræðuefni + Bættu við umræðuefni Sannprófa þessa innskráningu Skrá út úr þessari setu Skilaboð við þennan notanda eru enda-í-enda dulrituð þannig að enginn annar getur lesið þau. @@ -1751,7 +1751,7 @@ Forritarahamur Hreinsa persónuleg gögn Taktu þátt ókeypis ásamt milljónum annarra á stærsta almenningsþjóninum - sleppt þessari spurningu + Sleppa þessari spurningu Örugg skilaboð. Gat ekki tengst við auðkennisþjón Dulritunarlyklarnir þínir eru ekki öryggisafritaðir úr þessari setu. @@ -1990,10 +1990,10 @@ Séð af Sleppa þessu skrefi Vista og halda áfram - Kjörstillingarnar þínar hafa verið vistaðar. + Farðu hvenær sem er í stillingarnar til að breyta notandasniðinu þínu. Nú ertu tilbúin(n)! Hefjumst handa - Þú getur breytt þessu hvenær sem er. + Þú getur breytt þessu hvenær sem er Bættu við auðkennismynd Þú getur breytt þessu síðar Birtingarnafn @@ -2012,4 +2012,206 @@ Prófaðu það Gera óvirkt Upphafleg samstillingarbeiðni - + Velkomin í nýja sýn! + Skoða staðsetningu í rauntíma + Sumar niðurstöður gætu verið faldar þar sem þær eru einkamál, þá þarftu boð til að geta séð þær. + Þú ert eini stjórnandi þessa svæðis. Ef þú yfirgefur það verður enginn annar sem er með stjórn yfir því. + Þú munt ekki geta tekið þátt aftur nema þér verði boðið aftur. + Yfirgefa ekkert + Yfirgefa allt + Efni á þessu svæði + Þetta samnefni er ekki aðgengilegt í augnablikinu. +\nPrófaðu aftur síðar, eða spurðu einhvern stjórnanda hvort þú hafir aðgang. + Fara af spjallrás með uppgefið auðkenni (eða fyrirliggjandi spjallrás ef þetta er núll) + Taka þátt í svæði með uppgefið auðkenni + Gat ekki virkjað auðkenningu með lífkennum. + Annars geturðu sett inn slóð á hvaða auðkennisþjón sem er + Heimaþjónninn þinn (%1$s) stingur upp á að nota %2$s sem auðkenningarþjón fyrir þig + Samþykki notandans hefur ekki verið gefið. + Stilltu fyrst auðkennisþjón. + Þessi aðgerð er ekki möguleg. Heimaþjónninn er úreltur. + Deildu þessum kóða með fólki svo viðkomandi geti skannað hann, bætt þér við og byrjað að spjalla. + Heimaþjónn notandans samþykkir ekki notendanöfn einungis með tölustöfum. + Hindra skjámyndatöku af forritinu + Uppsetning tilkynninga + Mistókst að flytja inn lykla + Næstum því búið! Sýnir hitt tækið gátmerki\? + %s svo fólk viti að um hvað málin snúist. + Sendu fyrstu skilaboðin þín til að bjóða %s að spjalla + Þetta er upphafið á þessu samtali. + Þetta er upphafið á %s. + %s bjó til og stillti spjallrásina. + Dulritunin sem notuð er í þessari spjallrás er ekki studd + Dulritun er rangt stillt + Skilaboð í þessu spjalli verða enda-í-enda dulrituð. + Skilaboð í þessari spjallrás eru enda-í-enda dulrituð. Lærðu meira um þetta og yfirfarðu notendur í notandasniðum þeirra. + Ef þú hættir við núna, geturðu tapað dulrituðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum. +\n +\nÞú getur víka sett upp örugga afritun og sýslað með dulritunarlyklana þína í stillingunum. + Gef út útbúna auðkennislykla + Set upp endurheimtu. + Ekki nota lykilorðið fyrir aðganginn þinn. + Lykill skilaboða + Þetta var ekki ég + Beiðnir um lykla + ${app_name} fyrir Android + Næstum því búið! Sýnir %s gátmerki\? + Mistókst að ná í setur + + %d virk seta + %d virkar setur + + Engar dulkóðunarupplýsingar tiltækar + Þú hefur ekki heimild til að virkja dulritun á þessari spjallrás. + Kóði var sendur til: %s + Staðfestu símanúmerið þitt + Staðfestingarkóði + Viltu hýsa þinn eigin netþjón\? + Hvert er vistfang netþjónsins þíns\? + Hvert er vistfang netþjónsins þíns\? Þetta er staður sem geymir öll gögnin þín + Veldu netþjón fyrir þig + Þar sem samtölin þín eru + Þar sem samtölin þín verða + Verður að vera að minnsta kosti 8 stafir + Aðrir geta fundið þig %s + %s aðgangur þinn hefur verið útbúinn + Fara á forsíðuna + Persónugera notandasnið + Ætlarðu að ganga til liðs við fyrirliggjandi netþjón\? + Ekki ennþá viss\? %s + Við hverja muntu helst spjalla\? + ${app_name} er líka frábært fyrir vinnustaðinn. Heimsins öruggustu samtök treysta því. + Enda-í-enda dulritað og ekkert símanúmer nauðsynlegt. Engar auglýsingar eða gagnasöfnun. + Veldu hvar á að geyma samtölin þín, sem gefur þér stjórnina og algert sjálfstæði. Tengt í gegnum Matrix. + Örugg og óháð samskipti sem gefa þér færi á að ræða málin í friði rétt eins og þetta sé maður á mann í heimahúsi. + Skilaboð fyrir teymið þitt. + Skrifaðu stikkorð til að finna viðbrögð. + Opna svæðalista + Ekki er hægt að forskoða þessa spjallrás. Viltu taka þátt í henni\? + Þessi spjallrás er ekki aðgengileg í augnablikinu. +\nPrófaðu aftur síðar, eða spurðu einhvern stjórnanda hvort þú hafir aðgang. + Rangt sniðinn atburður, get ekki birt hann + Atburði eytt af notanda + Nýjir lyklar fyrir örugg skilaboð + Hjálpaðu okkur við að greina vandamál og bæta ${app_name} með því að deila nafnlausum gögnum varðandi notkun. Til að skilja hvernig fólk notar saman mörg tæki, munum við útbúa tilviljanakennt auðkenni, sem tækin þín deila. +\n +\nÞú getur lesið alla skilmála okkar %s. + Spila hreyfimyndir sjálfvirkt + Mistókst að skrá endapunkt á heimaþjóninn: +\n%1$s + Það tókst að skrá endapunkt á heimaþjóninn. + Skráning endapunkts + + %1$s og %2$d í viðbót + %1$s og %2$d í viðbót + + Skoða og uppfæra hlutverk sem krafist er til að breyta ýmsum þáttum svæðisins. + Skoða og uppfæra hlutverk sem krafist er til að breyta ýmsum þáttum spjallrásarinnar. + Tölvupóstfang ekki staðfest, athugaðu pósthólfið þitt + Ekkert nýtt. + Engin svæði ennþá. + Einfaldað Element með valkvæðum flipum + Virkja nýja framsetningu + Kjörstillingar framsetningar + Skipta um svæði + Allar spjallrásir + Prófaðu það + Gefðu umsögn + IP-vistfang + Síðasta virkni + Nafn á setu + Nánar um setuna + Hreinsa síu + Engar óvirkar setur fundust. + Engar óstaðfestar setur fundust. + Engar staðfestar setur fundust. + Óvirkt + Óstaðfest + Staðfest + Sía + Óvirkt + Óstaðfest + Staðfest + Allar setur + Sía + Síðasta virkni %1$s + Tæki + Seta + Núverandi seta + Óstaðfestar setur + Skoða allt (%1$d) + Skoða nánar + Sannprófa setu + Óstaðfest seta + Staðfest seta + Óþekkt tegund tækis + Skjáborð + Vefur + Farsími + Virkja deilingu staðsetninga + Netgátt + Aðferð + Samstilling í bakgrunni + Google þjónustur + Deila staðsetningu + %1$s hætti + Niðurstöður birtast einungis eftir að könnuninni hefur lokið + Engar niðurstöður fundust + Opna stillingar + Afritaðu hann á einkageymslu sem þú átt í tölvuskýi + Vistaðu hann á USB-lykil eða öryggisdisk + Prentaðu hann og geymdu á öruggum stað + Settu inn öryggisfrasa sem aðeins þú þekkir, þetta er notað til að verja leyndarmálin sem þú geymir á netþjóninum þínum. + Settu inn %s til að halda áfram. + Tókst ekki að sannreyna þetta tæki + Aðrar setur + Setur + Notandanafn / tölvupóstfang / símanúmer + Ertu mannvera\? + Endurstilling lykilorðs + Gleymt lykilorð + Senda tölvupóst aftur + Skoðaðu tölvupóstinn þinn + Endursenda kóða + Skrá út öll tæki + Endurstilla lykilorð + Veldu nýtt lykilorð + Nýtt lykilorð + Athugaðu tölvupóstinn þinn. + Símanúmer + Settu inn símanúmerið þitt + Tölvupóstur + Settu inn tölvupóstfangið þitt + Hafðu samband + Slóð netþjóns + Velkomin(n) aftur! + Breyta + Eða + Búa til aðganginn þinn + Við munum hjálpa þér að tengjast + Fara + Þessa spjallrás er ekki hægt að forskoða + Uppfæri gögnin þín… + Fólk + Eftirlæti + Ólesið + Allt + Nota sjálfgefnar kerfisstillingar + Velja handvirkt + Setja sjálfvirkt + Veldu leturstærð + %1$s og %2$s + Boðsgestir + A-Ö + Virkni + Raða eftir + Birta nýlegt + Sýna síur + Næsta + sek + mín + klst + Kanna spjallrásir + Búa til spjallrás + Hefja spjall + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index ecb29d1586..b2f9fa9238 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -430,7 +430,7 @@ Tema Errore di decriptazione Nome pubblico - ID sessione + ID sessione Chiave sessione Esporta le chiavi di crittografia E2E delle stanze Esporta le chiavi delle stanze @@ -2592,8 +2592,8 @@ Apri le impostazioni Tutte le chat Mostra tutte le sessioni (V2, WIP) - Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più. - Altre sessioni + Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più. + Altre sessioni Sessioni Apri elenco spazi Crea una nuova conversazione o stanza @@ -2613,11 +2613,8 @@ Non verificata · Ultima attività %1$s Verificata · Ultima attività %1$s Vedi tutte (%1$d) - Sessione attuale Vedi dettagli Verifica la sessione - Verifica la tua sessione attuale per messaggi più sicuri. - La tua sessione attuale è pronta per i messaggi sicuri. Sessione non verificata Sessione verificata Tipo di dispositivo sconosciuto @@ -2627,4 +2624,81 @@ Spiacenti, questa stanza non è stata trovata. \nRiprova più tardi.%s Inviti - + Provalo + Tocca in alto a destra per vedere l\'opzione feedback. + Invia un feedback + Accedi ai tuoi spazi (in basso a destra) più velocemente e più facilmente che mai. + Accedi agli spazi + Per semplificare ${app_name}, le schede ora sono opzionali. Gestiscile usando il menu in alto a destra. + Benvenuti ad una nuova panoramica! + Qui è dove verranno mostrati i messaggi non letti, quando ne avrai qualcuno. + Niente da segnalare. + L\'app di chat tutto-in-uno per team, amici e organizzazioni. Inizia una conversazione o entra in una stanza esistente per cominciare. + Benvenuto/a in ${app_name}, +\n%s. + Gli spazi sono un modo nuovo di raggruppare stanze e persone. Aggiungi una stanza esistente, o creane una nuova usando il pulsante in basso a destra. + %s +\nsembra un po\' vuoto. + + Considera di disconnettere le sessioni vecchie (%1$d giorno o più) che non usi più. + Considera di disconnettere le sessioni vecchie (%1$d giorni o più) che non usi più. + + Sessioni inattive + Verifica o disconnetti le sessioni non verificate. + Sessioni non verificate + Migliora la sicurezza del tuo account seguendo questi consigli. + Consigli di sicurezza + + Inattivo da %1$d+ giorno (%2$s) + Inattivo da %1$d+ giorni (%2$s) + + Qui è dove troverai le nuove richieste e gli inviti. + Niente di nuovo. + Gli spazi sono un modo nuovo di raggruppare stanze e persone. Crea uno spazio per iniziare. + Ancora nessuno spazio. + Riduci contenuto di %s + Espandi contenuto di %s + Cambia spazio + Indirizzo IP + Ultima attività + Nome sessione + Applicazione, dispositivo e informazioni di attività. + Dettagli sessione + Annulla filtro + Nessuna sessione inattiva trovata. + Nessuna sessione non verificata trovata. + Nessuna sessione verificata trovata. + + Considera di disconnettere le sessioni vecchie (%1$d giorno o più) che non usi più. + Considera di disconnettere le sessioni vecchie (%1$d giorni o più) che non usi più. + + Inattivo + Verifica le tue sessioni per avere conversazioni più sicure o disconnetti quelle che non riconosci o che non usi più. + Non verificato + Per una maggiore sicurezza, disconnetti tutte le sessioni che non riconosci o che non usi più. + Verificato + Filtra + + Inattivo da %1$d giorno o più + Inattivo da %1$d giorni o più + + Inattivo + Non pronto per messaggi sicuri + Non verificato + Pronto per messaggi sicuri + Verificato + Tutte le sessioni + Filtra + Ultima attività %1$s + Dispositivo + Sessione + Sessione attuale + Verifica o disconnetti questa sessione per una migliore sicurezza e affidabilità. + Verifica la tua sessione attuale per messaggi più sicuri. + Questa sessione è pronta per i messaggi sicuri. + La tua sessione attuale è pronta per i messaggi sicuri. + Attiva messaggi diretti differiti + Crea messaggio diretto solo al primo messaggio + Un Element semplificato con schede opzionali + Attiva nuova disposizione + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-iw/strings.xml b/library/ui-strings/src/main/res/values-iw/strings.xml index 6d9533852b..ff19310c8e 100644 --- a/library/ui-strings/src/main/res/values-iw/strings.xml +++ b/library/ui-strings/src/main/res/values-iw/strings.xml @@ -542,7 +542,7 @@ יצא מפתחות חדר ייצא מפתחות חדר E2E מזהה מפתח - מזהה מושב + מזהה מושב שם ציבורי שגיאת פענוח ערכת נושא diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index b781e4d7f0..3e817e398c 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -197,7 +197,7 @@ これらは予期しない不具合が生じるかもしれない実験的機能です。慎重に使用してください。 メインアドレスとして設定 メインアドレスとしての設定を解除 - セッションID + セッションID 文字の大きさ とても小さい 小さい diff --git a/library/ui-strings/src/main/res/values-kab/strings.xml b/library/ui-strings/src/main/res/values-kab/strings.xml index a79b72efde..353fb99f53 100644 --- a/library/ui-strings/src/main/res/values-kab/strings.xml +++ b/library/ui-strings/src/main/res/values-kab/strings.xml @@ -291,7 +291,7 @@ Talqayt Tinarimin Asentel - Asulay n tqimit + Asulay n tqimit Tasarut n tɣimit Sifeḍ tisura n texxamt E2E Sifeḍ tisura n texxamt diff --git a/library/ui-strings/src/main/res/values-ko/strings.xml b/library/ui-strings/src/main/res/values-ko/strings.xml index ba0cbe5abd..37e8849fa8 100644 --- a/library/ui-strings/src/main/res/values-ko/strings.xml +++ b/library/ui-strings/src/main/res/values-ko/strings.xml @@ -431,7 +431,7 @@ 테마 암호 복호화 오류 공개 이름 - ID + ID 기기 키 종단간 암호화 방 키 내보내기 방 키 내보내기 diff --git a/library/ui-strings/src/main/res/values-lo/strings.xml b/library/ui-strings/src/main/res/values-lo/strings.xml index 1a9a2820b8..a92adb0225 100644 --- a/library/ui-strings/src/main/res/values-lo/strings.xml +++ b/library/ui-strings/src/main/res/values-lo/strings.xml @@ -909,7 +909,7 @@ ສົ່ງອອກກະແຈຫ້ອງ ສົ່ງອອກກະແຈຫ້ອງ E2E ລະຫັດລະບົບ - ID ລະບົບ + ID ລະບົບ ຊື່ສາທາລະນະ ການຖອດລະຫັດຜິດພາດ ຫົວຂໍ້ diff --git a/library/ui-strings/src/main/res/values-lt/strings.xml b/library/ui-strings/src/main/res/values-lt/strings.xml index c33f8257c6..aeba3d53e6 100644 --- a/library/ui-strings/src/main/res/values-lt/strings.xml +++ b/library/ui-strings/src/main/res/values-lt/strings.xml @@ -1,5 +1,5 @@ - + Naudotojo %s pakvietimas Jūs prisijungėte prie kambario %1$s prisijungė prie kambario @@ -668,12 +668,12 @@ Pridėjote %1$s valdiklį %1$s pridėjo %2$s valdiklį %1$s išsiuntė kvietimą %2$s prisijungti prie kambario - %1$s atšaukė %2$s kvietimą prisijungti prie kambario + %1$s atšaukė %2$s pakvietimą prisijungti prie kambario Priėmėte kvietimą į %1$s %1$s priėmė kvietimą į %2$s Atšaukėte kvietimą %1$s %1$s atšaukė %2$s kvietimą - Atšaukėte %1$s kvietimą prisijungti prie kambario + Atšaukėte %1$s pakvietimą prisijungti prie kambario Pakeitimų nėra. • Serveriai atitinkantys %s buvo pašalinti iš leidžiamų sąrašo. • Serveriai atitinkantys %s dabar yra leidžiami. @@ -681,4 +681,1506 @@ • Serveriai atitinkantys %s dabar yra uždrausti. • Serveriai atitinkantys %s yra leidžiami. • Serveriai atitinkantys %s yra uždrausti. + Siųsti m.room.server_acl įvykius + Užblokuoti naudotoją + Naudotojas bus pašalintas iš šio kambario. +\n +\nKad jis negalėtų prisijungti dar kartą, turėtumėte jį užblokuoti. + Pašalinimo priežastis + Pašalinti naudotoją + Ar tikrai norite atšaukti kvietimą šiam naudotojui\? + Atšaukti kvietimą + Nebeignoruoti + Nebeignoruojant šio naudotojo, vėl bus rodomos visos jo žinutės. + Nebeignoruoti naudotojo + Ignoruodami šį naudotoją nebematysite jo žinučių bendruose kambariuose. +\n +\nŠį veiksmą bet kada galite atšaukti bendruosiuose nustatymuose. + Ignoruoti naudotoją + Pažeminti + Negalėsite atšaukti šio pakeitimo, nes pažeminsite save, o jei esate paskutinis privilegijuotas naudotojas kambaryje, bus neįmanoma susigrąžinti privilegijų. + Nuleisti save į žemesnes pareigas\? + Šio pakeitimo atšaukti negalėsite, nes padidinsite naudotojo galią, kad jis turėtų tokį patį galios lygį, kaip ir jūs pats. +\nAr esate tikri\? + Paminėti + Pašalinti iš pokalbio + Atšaukti kvietimą + Tiesioginiai pokalbiai + Šis kambarys nėra viešas. Negalėsite vėl prisijungti be kvietimo. + Ar tikrai norite palikti kambarį\? + Palikti kambarį + + %d narys + %d nariai + %d narių + + Pereiti prie neskaitytų + Nariai + Suteikite leidimą prieiti prie savo kontaktų. + Jei norite nuskaityti QR kodą, turite leisti kameros prieigą. + ${app_name} reikia leidimo prieiti prie jūsų kameros ir mikrofono, kad galėtumėte atlikti vaizdo skambučius. +\n +\nKad galėtumėte skambinti, kituose iškylančiuose languose leiskite prieigą. + Čia bus pateikiamos naujos užklausos ir kvietimai. + Nieko naujo. + Pradėti pokalbį + • Serveriai atitinkantys IP dabar yra užblokuoti. + • Serveriai atitinkantys IP dabar yra leidžiami. + + %d serverių ACL pakeitimas + %d serverių ACL pakeitimai + %d serverių ACL pakeitimų + + Pakeitėte serverių ACL šiam kambariui. + %s pakeitė serverių ACL šiam kambariui. + • Serveriai atitinkantys IP yra užblokuoti. + • Serveriai atitinkantys IP yra leidžiami. + Nustatėte serverių ACL šiam kambariui. + %s nustatė serverių ACL šiam kambariui. + Išbandyti + Bakstelėkite viršuje dešinėje, kad pamatytumėte atsiliepimų parinktį. + Pateikite atsiliepimus + Pasiekti erdves + Pasiekite erdves (apačioje dešinėje) greičiau ir paprasčiau nei bet kada anksčiau. + Siekiant supaprastinti jūsų ${app_name}, skirtukai dabar yra neprivalomi. Tvarkykite juos naudodami viršutinį dešinės pusės meniu. + Sveiki atvykę į naują vaizdą! + Čia bus rodomos jūsų neperskaitytos žinutės, kai jų turėsite. + Nėra apie ką pranešti. + \"viskas viename\" saugi pokalbių programėlė komandoms, draugams ir organizacijoms. Sukurkite pokalbį arba prisijunkite prie esamo kambario ir pradėkite. + Sveiki atvykę į ${app_name}, +\n%s. + Erdvės - tai naujas kambarių ir žmonių grupavimo būdas. Pridėkite esamą kambarį arba sukurkite naują naudodami apatinį dešinįjį mygtuką. + %s +\natrodo šiek tiek tuščia. + + Apsvarstykite galimybę atsijungti iš senų sesijų (%1$d diena ar daugiau), kurių nebenaudojate. + Apsvarstykite galimybę atsijungti iš senų sesijų (%1$d dienos ar daugiau), kurių nebenaudojate. + Apsvarstykite galimybę atsijungti iš senų sesijų (%1$d dienų ar daugiau), kurių nebenaudojate. + + Neaktyvios sesijos + Patvirtinkite nepatvirtintas sesijas arba atjunkite jas. + Nepatvirtintos sesijos + Pagerinkite savo paskyros saugumą laikydamiesi šių rekomendacijų. + Saugumo rekomendacijos + + Neaktyvus %1$d+ dieną (%2$s) + Neaktyvus %1$d+ dienas (%2$s) + Neaktyvus %1$d+ dienų (%2$s) + + Nepatvirtinta · Paskutinė veikla %1$s + Patvirtinta · Paskutinė veikla %1$s + Peržiūrėti visas (%1$d) + Peržiūrėti detales + Patvirtinti sesiją + Nepatvirtinta sesija + Patvirtinta sesija + Nežinomas įrenginio tipas + Stalinis kompiuteris + Naršyklė + Mobilus + + %d žinutė pašalinta + %d žinutės pašalintos + %d žinučių pašalinta + + Įjungti vietos bendrinimą + Atkreipkite dėmesį, kad tai yra laboratorinė funkcija, kuri įgyvendinama laikinai. Tai reiškia, kad negalėsite ištrinti savo buvimo vietos istorijos, o pažengę naudotojai galės matyti jūsų buvimo vietos istoriją net tada, kai nustosite bendrinti savo tiesioginę buvimo vietą su šiuo kambariu. + Tiesioginės buvimo vietos bendrinimas + Dabartiniai vartai: %s + Vartai + Nepavyksta rasti galinio taško. + Dabartinis galinis taškas: %s + Galinis taškas + Šiuo metu naudojamas %s. + Metodas + + Rastas %d metodas. + Rasti %d metodai. + Rasti %d metodų. + + Nerastas joks kitas metodas, išskyrus foninį sinchronizavimą. + Nerastas joks kitas būdas, išskyrus Google Play paslaugas. + Galimi metodai + Pranešimo metodas + Foninis sinchronizavimas + Google Paslaugos + Pasirinkite, kaip gauti pranešimus + Vyksta ekrano bendrinimas + ${app_name} Ekrano bendrinimas + Kambario pranešimas + Naudotojai + Pranešti visam kambariui + + %1$d daugiau + %1$d daugiau + %1$d daugiau + + Rodyti mažiau + Bendrinti vietą + Kurti apklausą + Atidaryti kontaktus + Siųsti lipduką + Įkelti failą + Reikalingas atnaujinimas + Atnaujinti + Būkite kantrūs, tai gali užtrukti. + Prisijungti prie pakaitinio kambario + Nepavadintas kambarys + Jūs esate vienintelis šios erdvės administratorius. Jei ją paliksite, tai reikš, kad niekas jos nebekontroliuos. + Negalėsite prisijungti vėl, nebent būsite pakviestas iš naujo. + Jūs esate vienintelis čia esantis asmuo. Jei paliksite, ateityje niekas, įskaitant jus, negalės prisijungti. + Ar tikrai norite palikti %s\? + Nepalikti nė vieno + Palikti visus + Dalykai šioje erdvėje + Vistiek prisijungti + Kol kas praleisti + Baigti nustatyti atradimą. + Šiuo metu šis pseudonimas neprieinamas. +\nPabandykite vėliau arba paprašykite kambario administratoriaus patikrinti, ar turite prieigą. + Atradimas (%s) + Užbaigti sąranka + Kvieskite el. paštu, ieškokite kontaktų ir daugiau… + Jie nebus %s dalis + Tik į šį kambarį + Šiuo metu nenaudojate tapatybės serverio. Norėdami pakviesti komandos draugus ir būti jų atrandami, sukonfigūruokite jį toliau. + Prisijungti prie erdvės + Sukurti erdvę + Prisijunkite prie mano erdvės %1$s %2$s + Jie galės tyrinėti %s + Pakvietimas į %s + Kviesti žmones į savo erdvę + Aprašymas + Kuriama erdvė… + Atsitiktinis + Bendra + Sukurkime kiekvienai iš jų po kambarį. Vėliau galite pridėti ir daugiau, įskaitant jau esamus. + Su kokiais dalykais dirbate\? + Užtikrinkite, kad prieigą prie %s kompanijos turėtų tinkami žmonės. Vėliau galite pakviesti daugiau. + Kas yra jūsų komandos draugai\? + Mes sukursime joms kambarius. Vėliau galėsite pridėti ir daugiau. + Kokias diskusijas norite turėti %s\? + Suteikite jai pavadinimą, kad galėtumėte tęsti. + Pridėkite šiek tiek detalių, kad žmonės galėtų ją atpažinti. Jas galite keisti bet kuriuo metu. + Įtraukite keletą detalių, kurios padės išsiskirti. Jas galite keisti bet kuriuo metu. + Sukurti erdvę + Privati erdvė jums & jūsų komandos draugams + Aš ir komandos draugai + Privati erdvė kambariams organizuoti + Tik aš + Užtikrinkite, kad prieigą prie %s turėtų tinkami asmenys. + Su kuo dirbate\? + Norėdami prisijungti prie esamos erdvės, turite gauti kvietimą. + Galite tai pakeisti vėliau + Kokio tipo erdvę norite sukurti\? + Jūsų privati erdvė + Jūsų vieša erdvė + Pridėti erdvę + Privati erdvė + Vieša erdvė + Prisijungti prie erdvės su nurodytu id + Pridėti prie nurodytos erdvės + Sukurti erdvę + Neteisingas naudotojo vardas ir (arba) slaptažodis. Įvestas slaptažodis prasideda arba baigiasi tarpais, patikrinkite jį. + Kuriama erdvė… + Erdvės adresas + Negalima atidaryti šios nuorodos: bendruomenės buvo pakeistos erdvėmis + Atidaryti erdvių sąrašą + Naudojate erdvių beta versiją. Jūsų atsiliepimai padės parengti kitas versijas. Jūsų platforma ir naudotojo vardas bus pažymėti, kad galėtume kuo geriau pasinaudoti jūsų atsiliepimais. + Atsiliepimai apie erdves + Sukurti naują erdvę + Kitos erdvės ar kambariai, apie kuriuos galbūt nežinote + Erdvė, apie kurią žinote, kurioje yra šis kambarys + Bakstelėkite, kad redaguoti erdves + Pasirinkti erdves + Nuspręskite, kurios erdvės gali prieit prie šio kambario. Jei pasirinkta erdvė, jos nariai galės rasti kambario pavadinimą ir prie jo prisijungti. + Erdvės, kurios gali pasiekti + Leisti erdvės nariams rasti ir pasiekti. + Erdvės %s nariai gali rasti, peržiūrėti ir prisijungti. + Kiekvienas, esantis erdvėje, kurioje yra šis kambarys, gali jį rasti ir prie jo prisijungti. Tik šio kambario administratoriai gali jį įtraukti į erdvę. + Tik erdvės nariams + Bet kas gali rasti šią erdvę ir prisijungti + Peržiūrėti ir valdyti šios erdvės adresus. + Erdvės adresai + Erdvės prieiga + + %1$s ir %2$d kitas + %1$s ir %2$d kiti + %1$s ir %2$d kitų + + Atnaujinti erdvę + Keisti erdvės pavadinimą + Įjungti erdvės šifravimą + Keisti erdvės pagrindinį adresą + Keisti erdvės avatarą + Neturite leidimo atnaujinti roles, reikalingas įvairioms šios erdvės dalims keisti + Pasirinkite roles, reikalingas įvairioms šios erdvės dalims keisti + Peržiūrėkite ir atnaujinkite roles, reikalingas įvairioms erdvės dalims keisti. + Erdvės leidimai + Atblokavus naudotoją, jis vėl galės prisijungti prie erdvės. + Užblokavus naudotoją, jis bus pašalintas iš šios erdvės ir negalės prisijungti dar kartą. + Naudotojas bus pašalintas iš šios erdvės. +\n +\nKad jis negalėtų prisijungti dar kartą, turėtumėte jį užblokuoti. + Erdvės + Erdvės - tai naujas kambarių ir žmonių grupavimo būdas. Sukurkite erdvę ir pradėkite. + Erdvių dar nėra. + Erdvės + Keisti erdvę + Atsarginė kopija turi galiojantį parašą iš nepatvirtintos sesijos %s + Atsarginė kopija turi galiojantį parašą iš patvirtintos sesijos %s. + Atsarginė kopija turi galiojantį šios sesijos parašą. + Atsarginė kopija turi galiojantį šio naudotojo parašą. + Atsarginė kopija turi nežinomos sesijos parašą su ID %s. + Jūsų raktų atsarginės kopijos iš šios sesijos nedaromos. + Šioje sesijoje raktų atsarginė kopija nėra aktyvi. + Šiai sesijai teisingai nustatyta atsarginė raktų kopija. + Ištrinti atsarginę kopiją + Atkurti iš atsarginės kopijos + Nepavyko gauti naujausios atkūrimo raktų versijos (%s). + + %d naujas raktas buvo pridėtas prie šios sesijos. + %d nauji raktai buvo pridėti prie šios sesijos. + %d naujų raktų buvo pridėta prie šios sesijos. + + + Atkurta atsarginė kopija su %d raktu. + Atkurta atsarginė kopija su %d raktais. + Atkurta atsarginė kopija su %d raktų. + + Atkurta atsarginė kopija %s ! + Atsarginės kopijos nepavyko iššifruoti naudojant šį atkūrimo raktą: patikrinkite, ar įvedėte teisingą atkūrimo raktą. + Įveskite atkūrimo raktą + Atrakinti istoriją + Importuojami raktai… + Atsisiunčiami raktai… + Apskaičiuojame atkūrimo raktą… + Atkuriama atsarginė kopija: + Atsarginės kopijos nepavyko iššifruoti naudojant šią slaptafrazę: patikrinkite, ar įvedėte teisingą atkūrimo slaptafrazę. + Pametėte atkūrimo raktą\? Galite nustatyti naują nustatymuose. + Įvesti atkūrimo raktą + Naudoti atkūrimo raktą, kad atrakinti užšifruotų žinučių istoriją + Nežinote savo atkūrimo slaptafrazės, galite %s. + naudokite savo atkūrimo raktą + Naudokite atkūrimo slaptafrazę, kad atrakintumėte užšifruotų žinučių istoriją + Gauname atsarginę versiją… + Jei atsijungsite arba prarasite šį prietaisą, galite prarasti prieigą prie savo žinučių. + Ar esate tikri\? + Netikėta klaida + Atkūrimo raktas + Generuojame atkūrimo raktą naudojant slaptafrazę, šis procesas gali užtrukti kelioliką sekundžių. + Bendrinti atkūrimo raktą su… + Prašome pasidaryti kopiją + Sustabdyti + Pakeisti + Atrodo, kad jau esate sukūrę atsarginę raktų kopiją iš kitos sesijos. Ar norite ją pakeisti kuriama\? + Jūsų namų serveryje jau yra atsarginė kopija + Atkūrimo raktas buvo išsaugotas. + Įrašyti kaip failą + Bendrinti + Išsaugoti atkūrimo raktą + Padariau kopiją + Baigta + Atkūrimo raktą laikykite labai saugioje vietoje, pvz., slaptažodžių tvarkyklėje (arba seife) + Atkūrimo raktas yra apsauginis tinklas - juo galite atkurti prieigą prie užšifruotų žinučių, jei pamiršite slaptafrazę. +\nAtkūrimo raktą laikykite labai saugioje vietoje, pvz., slaptažodžių tvarkyklėje (arba seife) + Jūsų raktų atsarginė kopija yra kuriama. + Sėkmė ! + (Išplėstinė) Nustatyti su atkūrimo raktu + Arba apsaugokite atsarginę kopiją naudodami atkūrimo raktą ir išsaugokite ją saugioje vietoje. + Atsarginės kopijos kūrimas + Nustatyti slaptafrazę + Jūsų namų serveryje išsaugosime šifruotą raktų kopiją. Apsaugokite atsarginę kopiją slaptafraze, kad ji būtų saugi. +\n +\nSiekiant maksimalaus saugumo, ji turėtų skirtis nuo jūsų paskyros slaptažodžio. + Apsaugokite atsarginę kopiją slaptafraze. + Eksportuoti raktus rankiniu būdu + (Išplėstiniai) + Pradėti naudoti raktų atsarginį kopijavimą + Užšifruotuose kambariuose siunčiamos žinutės yra apsaugotos šifravimu nuo galo iki galo. Tik jūs ir gavėjas (-ai) turite raktus, kad galėtumėte perskaityti šias žinutes. +\n +\nSaugiai kurkite atsargines raktų kopijas, kad jų neprarastumėte. + Niekada nepraraskite užšifruotų žinučių + Ištrinkite slaptafrazę, jei norite, kad ${app_name} sugeneruotų atkūrimo raktą. + Slaptafrazė yra per silpna + Įveskite slaptafrazę + Slaptafrazė nesutampa + Įvesti slaptafrazę + Patvirtinti slaptafrazę + Sukurti slaptafrazę + Nerastas galiojantis Google Play Paslaugų APK. Pranešimai gali neveikti tinkamai. + +%d + %1$s: %2$s + suskleisti + išplėsti + Atsiprašome, įvyko klaida + Jei norite toliau naudotis šia paslauga, prašome %s. + Prašome %s kad padidinti šią ribą. + Šis namų serveris pasiekė savo mėnesio aktyviųjų naudotojų limitą. + Šis namų serveris pasiekė mėnesio aktyviųjų naudotojų limitą, todėl kai kurie naudotojai negalės prisijungti. + Šis namų serveris viršijo vieną iš savo išteklių limitų. + Šis namų serveris viršijo vieną iš savo išteklių limitų, todėl kai kurie naudotojai negalės prisijungti. + kreipkitės į savo paslaugų administratorių + Spustelėkite čia, kad pamatytumėte senesnes žinutes + Šis kambarys yra kito pokalbio tęsinys + Pokalbis tęsiamas čia + Šis kambarys buvo pakeistas ir nebėra aktyvus. + Įveskite savo slaptažodį. + Įveskite naudotojo vardą. + Deaktyvuoti paskyrą + Prašau pamiršti visas mano išsiųstas žinutes, kai mano paskyra bus deaktyvuota (Įspėjimas: dėl to būsimi naudotojai matys nepilną pokalbių vaizdą) + Dėl to jūsų paskyra visam laikui taps netinkama naudoti. Negalėsite prisijungti ir niekas negalės iš naujo užregistruoti to paties naudotojo ID. Dėl to jūsų paskyra išeis iš visų kambarių, kuriuose dalyvauja, ir iš jūsų tapatybės serverio bus pašalinti jūsų paskyros duomenys. Šis veiksmas yra negrįžtamas. +\n +\nDeaktyvavus paskyrą pagal numatytuosius nustatymus nepamirštame jūsų išsiųstų žinučių. Jei norite, kad pamirštume jūsų žinutes, pažymėkite toliau esantį langelį. +\n +\nŽinučių matomumas Matrix sistemoje yra panašus į el. pašto matomumą. Mūsų jūsų žinučių užmiršimas reiškia, kad jūsų išsiųstomis žinutėmis nebus dalijamasi su jokiais naujais ar neregistruotais naudotojais, tačiau registruoti naudotojai, kurie jau turi prieigą prie šių žinučių, vis tiek turės prieigą prie jų kopijos. + Deaktyvuoti paskyrą + Peržiūrėti dabar + Norėdami toliau naudoti %1$s namų serverį, turite peržiūrėti ir sutikti su nuostatomis ir sąlygomis. + Avataras + Priežastis: %1$s + %2$s jus užblokavo iš %1$s + %2$s jus pašalino iš %1$s + Pakviestas + Kambariai + Pradžia + Sukurti + Šifruota žinutė + Triukšmingas + Tylus + Jūs neturite leidimo tai daryti šiame kambaryje. + Jūs nesate šiame kambaryje. + Pridėti Matrix programėlių + Trūksta reikalingo parametro. + Kambarys %s nėra matomas. + Blokuoti visus + Leisti + Kambario ID + Naudoti mikrofoną + Naudoti kamerą + Tvarkyti integracijas + Jūsų nepatvirtinta sesija \'%s\' prašo šifravimo raktų. + Nauja sesija prašo šifravimo raktų. +\nSesijos pavadinimas: %1$s +\nPaskutinį kartą matyta: %2$s +\nJei neprisijungėte prie kitos sesijos, ignoruokite šią užklausą. + Pridėjote naują sesiją \'%s\', kuri prašo šifravimo raktų. + Nepatvirtinta sesija prašo šifravimo raktų. +\nSesijos pavadinimas: %1$s +\nPaskutinį kartą matyta: %2$s +\nJei neprisijungėte prie kitos sesijos, ignoruokite šią užklausą. + Nustatyti naudotojo galios lygį + Nustoja ignoruoti naudotoją ir rodo jo žinutes nuo dabar + ignoruoja naudotoją, slepiant jo žinutes nuo jūsų + Atblokuoja naudotoją su nurodytu id + Užblokuoja naudotoją su nurodytu id + Rodo veiksmą + Prisijungia prie kambario su nurodytu adresu + Pakviečia naudotoją su nurodytu id į šį kambarį + Nustato kambario pavadinimą + Markdown buvo išjungtas. + Markdown buvo įjungtas. + Rodo informaciją apie naudotoją + Pakeičia šio kambario avatarą + Pakeičia jūsų rodomą slapyvardį tik šiame kambaryje + Pakeičia jūsų avatarą tik šiame kambaryje + Pakeičia jūsų rodomą slapyvardį + Pašalina naudotoją su nurodytu id iš šio kambario + Nustatyti kambario temą + Palikti kambarį + Neatpažinta komanda: %s + Pradėti patvirtinimą + Komandos klaida + Ignoruoti + Bendrinti + Komanda \"%s\" atpažįstama, bet nepalaikoma temose. + Komandai \"%s\" reikia daugiau parametrų arba kai kurie parametrai yra neteisingi. + + %d pasirinktas + %d pasirinkti + %d pasirinktų + + Keisti temą + Atnaujinti kambarį + Numatyta rolė + Neturite leidimo atnaujinti roles, reikalingas įvairioms kambario dalims keisti + Pasirinkite roles, reikalingas įvairioms kambario dalims keisti + Leidimai + Peržiūrėti ir atnaujinti roles, reikalingas įvairioms kambario dalims keisti. + Kambario leidimai + Sertifikatą priimkite tik tuo atveju, jei serverio administratorius yra paskelbęs antspaudą, atitinkantį pirmiau nurodytą. + Sertifikatas pakeistas iš anksčiau patikimo į nepatikimą. Serveris galėjo atnaujinti savo sertifikatą. Kreipkitės į serverio administratorių dėl numatyto antspaudo. + Sertifikatas pakeistas iš to, kuriuo pasitikėjo jūsų telefonas. Tai LABAI NEĮPRASTA. Rekomenduojama NEPATVIRTINTI šio naujo sertifikato. + Jei serverio administratorius nurodė, kad to tikimasi, įsitikinkite, kad toliau pateiktas antspaudas atitinka jo pateiktą antspaudą. + Tai gali reikšti, kad kažkas piktavališkai perima jūsų duomenų srautą arba kad telefonas nepasitiki nuotolinio serverio pateiktu sertifikatu. + Nepavyko patvirtinti nuotolinio serverio tapatybės. + Antspaudas (%s): + Ignoruoti + Nepasitikėti + Pasitikėti + + %d nauja žinutė + %d naujos žinutės + %d naujų žinučių + + Šifravimas buvo neteisingai sukonfigūruotas, todėl negalite siųsti žinučių. Spustelėkite, kad atidarytumėte nustatymus. + Šifravimas buvo neteisingai sukonfigūruotas, todėl negalite siųsti žinučių. Susisiekite su administratoriumi, kad būtų atkurta galiojanti šifravimo būsena. + Jūs neturite leidimo rašyti šiame kambaryje. + %1$s, %2$s ir kiti + %1$s ir %2$s + %1$s & %2$s & kiti rašo… + %1$s & %2$s rašo… + %s rašo… + Atblokavus naudotoją, jis vėl galės prisijungti prie kambario. + Užblokavus naudotoją, jis bus pašalintas iš šio kambario ir negalės prisijungti dar kartą. + Atblokuoti naudotoją + Priežastis užblokavimui + Išplėstiniai + Kita + Ignoruojami naudotojai + Naudotojo nustatymai + Išvalyti medijos talpyklą + Išvalyti talpyklą + Saugoti mediją + Privatumo politika + Autorinės teisės + Trečiųjų šalių pastabos + Terminai ir sąlygos + + %d sekundė + %d sekundės + %d sekundžių + + Uždelsimas tarp kiekvieno sinchronizavimo + Sinchronizavimo užklausos laiko limitas + Paleisti sistemos paleidimo metu + Kai programėlė yra fone, apie gautus pranešimus nebūsite informuojami. + Nėra foninio sinchronizavimo + ${app_name} sinchronizuosis fone periodiškai tiksliai nustatytu laiku (galima konfigūruoti). +\nTai turės įtakos radijo ryšio ir baterijos naudojimui, bus rodomas nuolatinis pranešimas, kad ${app_name} klausosi įvykių. + Optimizuotas realiajam laikui + ${app_name} bus sinchronizuojama fone taip, kad būtų tausojami riboti įrenginio ištekliai (akumuliatorius). +\nPriklausomai nuo įrenginio išteklių būklės, operacinė sistema gali atidėti sinchronizavimą. + Optimizuotas akumuliatoriui + Foninio sinchronizavimo režimas + Foninis sinchronizavimas + Mobiliuosiuose įrenginiuose negausite pranešimų apie užšifruotuose kambariuose esančius paminėjimus ir raktažodžius. + Kambario atnaujinimai + Boto žinutės + Kvietimai skambinti + Kvietimai į kambarį + Raktažodžiai + \@kambarys + Šifruotos grupių žinutės + Grupių žinutės + Šifruotos tiesioginės žinutės + Tiesioginės žinutės + Mano naudotojo vardas + Mano rodomas vardas + Žinutės, kuriose yra @room + Boto išsiųstos žinutės + Kai mane pakviečia į kambarį + Žinutės grupiniuose pokalbiuose + Žinutės pokalbiuose vienas su vienu + Žinutės, kuriose yra mano naudotojo vardas + Žinutės, kuriose yra mano rodomas vardas + Kai kambariai atnaujinami + Šifruotos žinutės grupiniuose pokalbiuose + Šifruotos žinutės pokalbiuose vienas su vienu + Pasirinkti LED spalvą, vibraciją, garsą… + Tyliųjų pranešimų konfigūravimas + Skambučių pranešimų konfigūravimas + Triukšmingų pranešimų konfigūravimas + Įjungti pranešimus šiai sesijai + Įjungti pranešimus šiai paskyrai + Pranešimo garsas + Ignoruoti optimizavimą + Jei naudotojas kurį laiką palieka prietaisą atjungtą nuo elektros tinklo ir nejudantį, su išjungtu ekranu, prietaisas įjungia \"Doze\" režimą. Tai neleidžia programoms prisijungti prie tinklo ir atideda jų darbus, sinchronizavimą ir standartinius žadintuvus. + Akumuliatoriaus optimizavimas neįtakoja ${app_name}. + Akumuliatoriaus optimizavimas + Išjungti apribojimus + Įjungti foniniai apribojimai ${app_name}. +\nDarbas, kurį programa bando atlikti, bus agresyviai ribojamas, kol ji yra fone, ir tai gali turėti įtakos pranešimams. +\n%1$s + Fono apribojimai išjungti ${app_name}. Šis testas turėtų būti atliekamas naudojant mobiliuosius duomenis (be WIFI). +\n%1$s + Patikrinti fono apribojimus + Įjungti paleidimą sistemos paleidimo metu + Paslauga nebus paleista iš naujo paleidus įrenginį, pranešimų negausite, kol vieną kartą nebus atidaryta ${app_name}. + Paslauga bus paleista iš naujo paleidus įrenginį. + Paleisti sistemos paleidimo metu + Pranešimas buvo paspaustas! + Spustelėkite pranešimą. Jei pranešimo nematote, patikrinkite sistemos nustatymus. + Pranešimo rodymas + Jūs žiūrite pranešimą! Spausk ant manęs! + Kai kurie pranešimai yra išjungti pasirinktiniuose nustatymuose. + Atkreipkite dėmesį, kad kai kurie pranešimų tipai nustatyti kaip tylūs (pranešimas bus be garso). + Pasirinktiniai nustatymai. + Šioje sesijoje pranešimai neįjungti. +\nPatikrinkite ${app_name} nustatymus. + Šioje sesijoje pranešimai yra įjungti. + Sesijos nustatymai. + Pranešimai jūsų paskyroje yra išjungti. +\nPatikrinkite paskyros nustatymus. + Jūsų paskyroje pranešimai yra įjungti. + Paskyros nustatymai. + Atidaryti nustatymus + Sistemos nustatymuose pranešimai yra išjungti. +\nPatikrinkite sistemos nustatymus. + Pranešimai yra įjungti sistemos nustatymuose. + Sistemos nustatymai. + Vienas ar daugiau testų nepavyko, pateikite pranešimą apie klaidą ir padėkite mums ją ištirti. + Vienas ar daugiau testų nepavyko, išbandykite siūlomą (-us) pataisymą (-us). + Pagrindinė diagnostika yra gera. Jei vis dar negaunate pranešimų, pateikite pranešimą apie klaidą ir padėkite mums ją ištirti. + Vykdoma… (%1$d iš %2$d) + Atlikti bandymus + Trikčių diagnostika + Pranešimų trikčių šalinimas + Raktažodžiai negali turėti \'%s\' + Raktažodžiai negali prasidėti su \'.\' + Pridėti naują raktažodį + Jūsų raktažodžiai + Praneškite man apie + Kita + Paminėjimai ir raktažodžiai + Numatyti pranešimai + Įjungti pranešimus el. paštu %s + Norėdami gauti pranešimą el. paštu, susiekite el. paštą su savo Matrix paskyra + Pranešimas el. paštu + Pranešimo svarba pagal įvykį + Išplėstiniai pranešimų nustatymai + Įsitikinkite, kad paspaudėte jums atsiųstame el. laiške esančią nuorodą. + Pašalinti %s\? + Telefono numeriai + Prie jūsų paskyros nepridėtas joks el. paštas + El. pašto adresai + Rodyti programos informaciją sistemos nustatymuose. + Programos informacija + Pridėti telefono numerį + Prie jūsų paskyros nepridėtas joks telefono numeris + Pridėti el. pašto adresą + Rodomas vardas + Profilio nuotrauka + Sesija buvo atjungta! + %1$s ir %2$s + Nėra rezultatų + Filtruoti užblokuotus narius + Filtruoti kambario narius + Ieškoti + Kambarys buvo paliktas! + Pridėti prie pagrindinio ekrano + Nėra + Tik paminėjimai & raktažodžiai + Visos žinutės + Filtruoti temas kambaryje + Temos artėja prie beta versijos 🎉 + Siųsti nuotraukas ir vaizdo įrašus + Atidaryti fotoaparatą + Rodyti žinučių burbulus + Tiesioginė buvimo vieta + Bendrinti vietą + Norėdami bendrinti tiesioginę buvimo vietą šiame kambaryje, turite turėti tinkamus leidimus. + Neturite leidimo bendrinti tiesioginę buvimo vietą + Atnaujinta prieš %1$s + Laikinas pritaikymas: vietos išlieka kambario istorijoje + Įjungti tiesioginį buvimo vietos bendrinimą + Bendrinama buvimo vieta + ${app_name} tiesioginė buvimo vieta + %1$s liko + Sustabdyti + Tiesiogiai iki %1$s + Žiūrėti tiesioginę buvimo vietą + Tiesioginė buvimo vieta baigėsi + Įkeliama tiesioginė vieta… + Tiesioginė buvimo vieta įjungta + Nepavyksta įkelti žemėlapio +\nŠis namų serveris gali būti nesukonfigūruotas rodyti žemėlapius. + Nepavyko įkelti žemėlapio + Atidaryti per + ${app_name} negalėjo pasiekti jūsų vietos. Prašome pabandyti vėliau. + ${app_name} negalėjo pasiekti jūsų vietos + 8 valandas + 1 valandą + 15 minučių + Bendrinti savo tiesioginę buvimo vietą + Bendrinti šią vietą + Bendrinti šią vietą + Bendrinti tiesioginę buvimo vietą + Bendrinti tiesioginę buvimo vietą + Bendrinti mano dabartinę vietą + Bendrinti mano dabartinę vietą + Priartinti esamą vietą + Pasirinktos vietos smeigtukas žemėlapyje + Žemėlapis + Rezultatai atskleidžiami tik tada, kai baigiate apklausą + Uždaryta apklausa + Balsuotojai mato rezultatus iškart po balsavimo + Atidaryti apklausą + Apklausos tipas + Redaguoti apklausą + Ar tikrai norite pašalinti šią apklausą\? Pašalinę ją negalėsite susigrąžinti. + Tai neleis žmonėms balsuoti ir bus rodomi galutiniai apklausos rezultatai. + Pašalinti apklausą + Apklausa baigėsi + Prabalsuota + Baigti apklausą + laimėtojo parinktis + Rezultatai bus matomi pasibaigus apklausai + Nėra balsų + Iš naujo paleiskite programą, kad pakeitimas įsigaliotų. + Įjungti LaTeX matematika + %s nustatymuose, kad gautumėte kvietimus tiesiogiai į ${app_name}. + Kvietimas į šią erdvę buvo išsiųstas į %s, kuris nėra susijęs su jūsų paskyra + Kvietimas į šį kambarį buvo išsiųstas į %s, kuris nėra susijęs su jūsų paskyra + Atkreipkite dėmesį, kad atnaujinus bus sukurta nauja kambario versija. Visos dabartinės žinutės liks šiame archyvuotame kambaryje. + Kiekvienas iš %s galės rasti šį kambarį ir prisijungti prie jo - nereikės visų kviesti rankiniu būdu. Tai galėsite bet kada pakeisti kambario nustatymuose. + (%1$s) + %1$s (%2$s) + Nepavyko paleisti %1$s + Pristabdyti %1$s + Paleisti %1$s + %1$d minutės %2$d sekundės + %1$s, %2$s, %3$s + %1$ds liko + Atsiprašome, bandant prisijungti įvyko klaida: %s + Atnaujinti į rekomenduojamą kambario versiją + Šiame kambaryje naudojama kambario versija %s, kurią šis namų serveris pažymėjo kaip nestabilią. + Norint atnaujinti kambarį, reikia leidimo + Jūs atnaujinsite šį kambarį iš %1$s į %2$s. + Kambario atnaujinimas yra išplėstinis veiksmas ir paprastai rekomenduojamas, kai kambarys yra nestabilus dėl klaidų, trūkstamų funkcijų ar saugumo spragų. +\nPaprastai tai turi įtakos tik tam, kaip kambarys apdorojamas serveryje. + Atnaujinti privatų kambarį + Atnaujinti viešą kambarį + Kai kurie kambariai gali būti paslėpti, nes yra privatūs ir į juos reikia pakvietimo. + Kai kurie kambariai gali būti paslėpti, nes yra privatūs ir į juos reikia pakvietimo. +\nJūs neturite leidimo pridėti kambarių. + Šioje erdvėje nėra kambarių + Dėl papildomos informacijos kreipkitės į savo namų serverio administratorių + Atrodo, kad jūsų namų serveris dar nepalaiko erdvių + Norite eksperimentuoti\? +\nĮ erdvę galite įtraukti esamas erdves. + Visi kambariai kuriuose esate, bus rodomi pradžioje. + Valdyti kambarius ir erdves + Pažymėti kaip nesiūlomą + Pažymėti kaip siūlomą + Siūlomas + Valdyti kambarius + Ieškote ko nors ne iš %s\? + %s kviečia tave + Rodyti naujausią profilio informaciją (avatarą ir rodomą vardą) visose žinutėse. + Rodyti naujausią naudotojo informaciją + Pastaba: programa bus paleista iš naujo + Įjungti temų žinutes + Jūsų sistema automatiškai išsiųs žurnalus, kai įvyks negalėjimo iššifruoti klaida + Automatiškai pranešti apie iššifravimo klaidas. + Erdvės - tai naujas kambarių ir žmonių grupavimo būdas. + Įtraukite erdvę į bet kurią valdomą erdvę. + Pridėti esamas erdves + Pridėti esamus kambarius + Pridėti esamus kambarius ir erdvę + Kai kurie rezultatai gali būti paslėpti, nes jie yra privatūs ir į juos reikia pakvietimo. + Rezultatų nerasta + Iš temos + Patarimas: Ilgai bakstelėkite žinutę ir naudokite “%s”. + Temos padeda išlaikyti pokalbių temą ir lengviau juos sekti. + Išlaikykite diskusijas organizuotas su temomis + Rodo visas temas, kuriose dalyvavote + Mano temos + Rodo visas temas iš dabartinio kambario + Visos temos + Filtras + Visos temos + Tema + Keisti leidimus + Keisti pagrindinį kambario adresą + Keisti kambario avatarą + Keisti valdiklius + Pranešti visiems + Pašalinti kitų išsiųstas žinutes + Užblokuoti naudotojus + Pašalinti naudotojus + Keisti nustatymus + Kviesti naudotojus + Siųsti žinutes + Įjungti kambario šifravimą + Keisti kambario pavadinimą + Keisti istorijos matomumą + %s atnaujino čia. + Atlikite captcha iššūkį + Pasirinkti pasirinktinį namų serverį + Pasirinkti Element Matrix Services + Pasirinkti matrix.org + Jūsų paskyra dar nesukurta. Sustabdyti registracijos procesą\? + Perspėjimas + Šis vartotojo vardas yra užimtas + Toliau + Slaptažodis + Naudotojo vardas + Naudotojo vardas arba el. pašto adresas + Toliau + Siųsti vėl + Įvesti kodą + Ką tik išsiuntėme kodą į %1$s. Įveskite jį toliau, kad patvirtintumėte, kad tai jūs. + Nustatyti telefono numerį + Neatrodo kaip tinkamas el. pašto adresas + Toliau + Patvirtinkite telefono numerį + El. pašto adresas (nebūtinas) + El. pašto adresas + Toliau + Telefono numeris (nebūtinas) + Jūsų slaptažodis buvo nustatytas iš naujo. + Sėkmė! + Patvirtinau savo el. pašto adresą + Bakstelėkite nuorodą ir patvirtinkite naująjį slaptažodį. Paspaudę joje esančią nuorodą, spustelėkite žemiau. + Patvirtinimo el. laiškas buvo išsiųstas į %1$s. + Patikrinkite savo pašto dėžutę + Šis el. paštas nėra susietas su jokia paskyra + Tęsti + Pakeitus slaptažodį bus iš naujo nustatyti visų jūsų sesijų visapusiško šifravimo raktai, todėl užšifruotų pokalbių istorijos nebus galima perskaityti. Prieš iš naujo nustatydami slaptažodį, sukurkite raktų atsarginę kopiją arba eksportuokite kambario raktus iš kitos sesijos. + Perspėjimas! + Naujas slaptažodis + El. paštas + Toliau + Į jūsų pašto dėžutę bus išsiųstas patvirtinimo el. laiškas, naujo slaptažodžio nustatymo patvirtinimui. + Iš naujo nustatyti slaptažodį %1$s + Šis el. paštas nesusijęs su jokia paskyra. + Programa negali sukurti paskyros šiame namų serveryje. +\n +\nAr norite užsiregistruoti naudodami žiniatinklio klientą\? + Atsiprašome, šis serveris nepriima naujų paskyrų. + Programa negali prisijungti prie šio namų serverio. Namų serveris palaiko šiuos prisijungimo tipus: %1$s. +\n +\nAr norite prisijungti naudodami žiniatinklio klientą\? + Įkeliant puslapį įvyko klaida: %1$s (%2$d) + Įveskite norimo naudoti serverio adresą + Įveskite adresą Modular Element arba serverio kurį norite naudoti + Aukščiausios kokybės talpinimas organizacijoms + Adresas + Element Matrix Services Adresas + Išvalyti istoriją + Tęsti su vienkartiniu prisijungimu + Prisijungti + Registruotis + Prisijungti prie %1$s + Prisijungti prie pasirinktinio serverio + Prisijungti prie Element Matrix Services + Prisijungti prie %1$s + Tęsti + vienkartinis prisijungimas + Prisijungti su %s + Užsiregistruoti su %s + Tęsti su %s + Arba + Pasirinktiniai & išplėstiniai nustatymai + Kitas + Sužinoti daugiau + Aukščiausios kokybės talpinimas organizacijoms + Nemokamai prisijunkite prie milijonų žmonių didžiausiame viešajame serveryje + Kaip ir el. paštas, paskyros turi vienus namus, nors galite bendrauti su bet kuo + Pasirinkti serverį + Aš jau turiu paskyrą + Sukurti paskyrą + Pradėkite + Išplėskite ir pritaikykite savo patirtį + Saugokite pokalbių privatumą naudodami šifravimą + Bendraukite su žmonėmis tiesiogiai arba grupėse + Tai jūsų pokalbis. Priklauso jums. + Praleisti šį žingsnį + Išsaugoti ir tęsti + Bet kada eikite į nustatymus norint atnaujinti savo profilį + Atrodo gerai! + Pirmyn + Laikas prie vardo pridėti veidą + Pridėti profilio nuotrauką + Jūs tai galite pakeisti vėliau + Rodomas vardas + Pasirinkite rodomą vardą + Vartotojo vardas / el. paštas / telefonas + Ar esate žmogus\? + Vykdykite nurodymus, išsiųstus adresu %s + Pamiršau slaptažodį + Slaptažodžio nustatymas iš naujo + Iš naujo siųsti el. laišką + Negavote el. laiško\? + Vykdykite nurodymus, išsiųstus adresu %s + Patvirtinkite savo el. pašto adresą + Iš naujo siųsti kodą + Kodas buvo išsiųstas į %s + Patvirtinkite savo telefono numerį + Atjungti visus prietaisus + Iš naujo nustatyti slaptažodį + Draugai ir šeima + Padėsime jums užmegzti ryšį + Su kuo daugiausiai bendrausite\? + ${app_name} taip pat puikiai tinka darbo vietoje. Ja pasitiki saugiausios pasaulio organizacijos. + Visapusiškai užšifruota ir nereikia telefono numerio. Jokių reklamų ar duomenų rinkimo. + Pasirinkite, kur bus saugomi jūsų pokalbiai, taip suteikdami jums galimybę kontroliuoti ir būti nepriklausomiems. Sujungta naudojant Matrix. + Saugus ir nepriklausomas bendravimas, suteikiantis tiek pat privatumo, kiek ir pokalbis akis į akį jūsų namuose. + Bandykite dar kartą, kai sutiksite su savo namų serverio nuostatomis ir sąlygomis. + Išsamūs žurnalai padės kūrėjams, nes siųsdami piktą purtymą pateiksite daugiau žurnalų. Net ir įjungus šią funkciją, programa nerenka žinučių turinio ar kitų privačių duomenų. + Įjungti išsamius žurnalus. + Sutikite su tapatybės serverio (%s) paslaugų teikimo sąlygomis, kad galėtumėte būti atrandami pagal el. pašto adresą arba telefono numerį. + Šiuo metu bendrinate el. pašto adresus arba telefono numerius tapatybės serveryje %1$s. Norėdami nustoti juos bendrinti, turėsite iš naujo prisijungti prie %2$s. + Tekstinė žinutė buvo išsiųsta adresu %s. Įveskite joje esantį patvirtinimo kodą. + Pasirinktame tapatybės serveryje nėra jokių paslaugų teikimo sąlygų. Tęskite tik tuo atveju, jei pasitikite paslaugos savininku + Tapatybės serveris neturi paslaugų teikimo sąlygų + Įveskite tapatybės serverio url + Nepavyko prisijungti prie tapatybės serverio + Įveskite tapatybės serverio URL + Ar sutinkate siųsti šią informaciją\? + Jei norite atrasti esamus kontaktus, į tapatybės serverį reikia nusiųsti kontaktinę informaciją (el. paštus ir telefono numerius). Prieš išsiunčiant duomenis, siekiant užtikrinti privatumą, juos sutriname. + Pateikti atsiliepimą + Pateikti atsiliepimą + Atsiliepimo nepavyko išsiųsti (%s) + Ačiū, jūsų atsiliepimas sėkmingai išsiųstas + Jei turite papildomų klausimų, galite susisiekti su manimi + Atsiliepimas + BETA + Pasiūlymo nepavyko išsiųsti (%s) + Ačiū, pasiūlymas sėkmingai išsiųstas + Aprašykite savo pasiūlymą čia + Žemiau parašykite savo pasiūlymą. + Pateikti pasiūlymą + Versijos + Gaukite pagalbos naudojant ${app_name} + Pagalba ir parama + Pagalba + Teisės aktai + Pagalba & Apie + Balsas & Vaizdas + Profilio žyma: + Formatas: + Url: + session_name: + app_display_name: + push_key: + app_id: + Jūs jau žiūrite šią temą! + Jūs jau žiūrite šį kambarį! + Importuoti šifravimo raktus iš failo \"%1$s\". + Įvyko klaida gaunant raktų atsarginės kopijos duomenis + Įvyko klaida gaunant pasitikėjimo informaciją + Kambarys sukurtas, tačiau kai kurie kvietimai nebuvo išsiųsti dėl šios priežasties: +\n +\n%s + Kiekvienas galės prisijungti prie šio kambario + Viešas + Tema + Kambario tema (nebūtina) + Pavadinimas + Kambario pavadinimas + Eiti + SUKURTI + Tiesioginės žinutės + Kambariai + Šio kambario negalima peržiūrėti. Ar norite prie jo prisijungti\? + Šiuo metu į šį kambarį patekti negalima. +\nPabandykite vėliau arba paprašykite kambario admino patikrinti, ar turite prieigą. + Šio kambario negalima peržiūrėti + Atnaujinami jūsų duomenys… + Prašome palaukti… + Keisti tinklą + Tinklo nėra. Patikrinkite interneto ryšį. + Sukurti naują kambarį + Neteisingai suformuotas įvykis, negalima rodyti + Įvykis moderuotas kambario admino + Naudotojo ištrintas įvykis + Žinutė pašalinta + Reakcijos + Peržiūrėti reakcijas + Pridėti reakciją + Reakcijos + Žmonės + Parankiniai + Neperskaityti + Visi + Čia bus rodomi jūsų kambariai. Bakstelėkite \"+\" apačioje dešinėje, kad rastumėte esamus kambarius arba pradėtumėte kurti savo. + Kambariai + Jūsų tiesioginių žinučių pokalbiai bus rodomi čia. Bakstelėkite \"+\" apačioje dešinėje, kad pradėtumėte keletą. + Pokalbiai + Neturite daugiau neperskaitytų žinučių + Jūs viską pasivijote! + Pakvietė %s + Išsiuntė jums kvietimą + Pakartoti + Peržiūrėti kambaryje + Atsakyti temoje + Atsakyti + Redaguoti + Atrodo, kad bandote prisijungti prie kito namų serverio. Ar norite atsijungti\? + Jūs nenaudojate jokio tapatybės serverio + Nežinoma klaida + %s nori patvirtinti jūsų sesiją + Patvirtinimo užklausa + Supratau + Patvirtinta! + Parašas + Algoritmas + Versija + + Kuriama atsarginė %d rakto kopija… + Kuriama atsarginė %d raktų kopija… + Kuriama atsarginė %d raktų kopija… + + Visų raktų atsarginė kopija sukurta + Nustatyti saugią atsarginę kopiją + Kuriama raktų atsarginė kopija. Tai gali užtrukti kelias minutes… + Valdyti raktų atsarginėje kopijoje + Nauji saugių žinučių raktai + Naudoti raktų atsarginę kopiją + Niekada nepraraskite užšifruotų žinučių + Apsisaugokite nuo užšifruotų žinučių ir duomenų praradimo + Saugi atsarginė kopija + Išjungta + Kad ištaisyti Matrix programėlių valdymą + Įj./Išj. markdown + Prašymas dalytis raktais + Atsiprašome, šis kambarys nerastas. +\nPrašome bandyti vėliau.%s + Jei norite tęsti, turite sutikti su šios paslaugos sąlygomis. + Nėra aktyvių valdiklių + Užklausoje trūksta user_id. + Užklausoje trūksta room_id. + Galios lygis turi būti teigiamas sveikasis skaičius. + Nepavyko išsiųsti užklausos. + Nepavyko sukurti valdiklio. + Skaityti DRM apsaugotą mediją + Šis valdiklis nori naudoti šiuos išteklius: + Palikti dabartinę konferenciją ir pereiti į kitą\? + Atsiprašome, bandant prisijungti prie konferencijos įvyko klaida + Atsiprašome, konferenciniai skambučiai su Jitsi nepalaikomi senuose įrenginiuose (įrenginiuose su žemesne nei 6.0 Android OS) + Valdiklio ID + Jūsų tema + Jūsų naudotojo ID + Jūsų avataro URL + Jūsų rodomas vardas + Atšaukti prieigą man + Atidaryti naršyklėje + Iš naujo įkelti valdiklį + Nepavyko įkelti valdiklio. +\n%s + Naudojant jį duomenys gali būti bendrinami su %s: + Naudojant jį gali būti nustatyti slapukai ir bendrinami duomenys su %s: + Šį valdiklį pridėjo: + Įkelti valdiklį + Valdiklis + Aktyvūs valdikliai + PERŽIŪRĖTI + + %d aktyvus valdiklis + %d aktyvūs valdikliai + %d aktyvių valdiklių + + Ar tikrai norite ištrinti valdiklį iš šio kambario\? + Milžiniškas + Didžiausias + Didesnis + Didelis + Vidutinis + Mažas + Mažytis + Šrifto dydis + Naudoti sistemos numatytąjį + Pasirinkti rankiniu būdu + Nustatyti automatiškai + Pasirinkti šrifto dydį + %1$s: %2$s %3$s + %1$s: %2$s + ** Nepavyko išsiųsti - atidarykite kambarį + + Naujas pakvietimas + Naujos žinutės + Kambarys + Naujas įvykis + %1$s ir %2$s + %1$s esantys %2$s ir %3$s + %1$s esantys %2$s + + %d pranešimas + %d pranešimai + %d pranešimų + + + %1$s: %2$d žinutė + %1$s: %2$d žinutės + %1$s: %2$d žinučių + + + %d pakvietimas + %d pakvietimai + %d pakvietimų + + + %d kambarys + %d kambariai + %d kambarių + + + %d neperskaityta pranešta žinutė + %d neperskaitytos praneštos žinutės + %d neperskaitytų praneštų žinučių + + Šis serveris jau yra sąraše + Negalima rasti šio serverio arba jo kambarių sąrašo + Įveskite naujo serverio, kurį norite patyrinėti, pavadinimą. + Pridėti naują serverį + Jūsų serveris + Visi vietiniai %s kambariai + Visi kambariai %s serveryje + Serverio pavadinimas + Pasirinkti kambarių katalogą + Jei jie nesutampa, gali kilti pavojus jūsų komunikacijos saugumui. + Patvirtinti + nežinomas ip + Patvirtinta + Nepatvirtinta + + %1$d/%2$d raktas importuotas sėkmingai. + %1$d/%2$d raktai importuoti sėkmingai. + %1$d/%2$d raktų importuota sėkmingai. + + Niekada nesiųsti užšifruotų žinučių į nepatvirtintas sesijas iš šios sesijos. + Šifruoti tik į patvirtintas sesijas + Importuoti + Importuoti raktus iš vietinio failo + Importuoti kambarių raktus + Importuoti šifruotų kambarių raktus + Užšifruotų žinučių atkūrimas + Raktai sėkmingai eksportuoti + Sukurkite slaptafrazę eksportuojamiems raktams užšifruoti. Norėdami importuoti raktus, turėsite įvesti tą pačią slaptafrazę. + Eksportuoti + Eksportuoti raktus į vietinį failą + Eksportuoti kambarių raktus + Eksportuoti šifruotų kambarių raktus + Sesijos raktas + Viešas pavadinimas + Iššifravimo klaida + Nuspręskite, kas gali rasti ir prisijungti prie šio kambario. + Nepavyko gauti dabartinio kambarių katalogo matomumo (%1$s). + Paskelbti šį kambarį viešai %1$s kambarių kataloge\? + Panaikinti šio adreso skelbimą + Paskelbti šį adresą + Pridėti vietinį adresą + Šis kambarys neturi vietinių adresų + Nustatykite šio kambario adresus, kad naudotojai galėtų rasti šį kambarį per jūsų namų serverį (%1$s) + Vietiniai adresai + Naujas skelbiamas adresas (pvz., #pseudonimas:serveris) + Kitų paskelbtų adresų dar nėra. + Kitų paskelbtų adresų dar nėra, pridėkite juos žemiau. + Ištrinti adresą \"%1$s\"\? + Panaikinti adreso \"%1$s\" skelbimą\? + Paskelbti + Paskelbti naują adresą rankiniu būdu + Kiti paskelbti adresai: + Tai yra pagrindinis adresas + Paskelbtus adresus gali naudoti bet kas bet kuriame serveryje, prisijungimui prie jūsų kambario. Norint paskelbti adresą, pirmiausia nustatykite jį kaip vietinį adresą. + Paskelbti adresai + Žetono registracija + Pridėti paskyrą + [%1$s] +\nŠi klaida yra nekontroliuojama ${app_name}. Telefone nėra Google paskyros. Atidarykite paskyrų tvarkytuvę ir pridėkite Google paskyrą. + Šifravimas neteisingai sukonfigūruotas + Šifravimas nėra įjungtas + Šiame pokalbyje žinutės bus visapusiškai užšifruojamos. + Šiame pokalbyje žinutės yra visapusiškai užšifruotos. + Šiame kambaryje žinutės yra visapusiškai užšifruotos. Sužinokite daugiau ir patvirtinkite naudotojus jų profilyje. + Šifravimas įjungtas + Šiame kambaryje naudojamas šifravimas nepalaikomas + Jau beveik! Laukiama patvirtinimo… + Jau beveik! Ar kitas prietaisas rodo varnelę\? + "Tema: " + Pridėkite temą + Siųskite pirmąją žinutę kad pakviestumėte %s į pokalbį + Tai yra jūsų tiesioginių žinučių su %s istorijos pradžia. + Tai šio pokalbio pradžia. + Tai yra %s pradžia. + Jūs prisijungėte. + %s prisijungė. + Sukūrėte ir sukonfigūravote kambarį. + %s sukūrė ir sukonfigūravo kambarį. + Nepavyko importuoti raktų + Laukiama %s… + Ši paskyra buvo deaktyvuota. + Žinutė… + Tikrinamas atsarginės kopijos raktas + Įveskite atkūrimo raktą + Tai netinkamas atkūrimo raktas + Naudoti failą + Norėdami tęsti, įveskite savo %s + Patvirtinkite save ir kitus, kad pokalbiai būtų saugūs + Galimas šifravimo patobulinimas + Tikrinamas atsarginės kopijos raktas (%s) + FCM žetonas sėkmingai užregistruotas namų serveryje. + Naudoti botus, tiltus, valdiklius ir lipdukų paketus + Keisti tapatybės serverį + Siųsti el. paštus ir telefono numerius + Konfigūruoti tapatybės serverį + Atjungti tapatybės serverį + Tapatybės serveris + Patvirtinimo kodas neteisingas. + Kodas + Atrodo, kad serveris neatsako per ilgai, tai gali būti dėl prasto ryšio arba serverio klaidos. Pabandykite dar kartą po kurio laiko. + %s perskaitė + %1$s ir %2$s perskaitė + %1$s, %2$s ir %3$s perskaitė + + %1$s, %2$s ir %3$d kitas perskaitė + %1$s, %2$s ir %3$d kiti perskaitė + %1$s, %2$s ir %3$d kitų perskaitė + + Peršokti į apačią + Uždaryti raktų atsarginės kopijos antraštę + Sukurti naują kambarį + Sukurti naują pokalbį arba kambarį + Sukurti naują tiesioginį pokalbį + Uždaryti kambario kūrimo meniu… + Atidaryti kambario kūrimo meniu + Atidaryti navigacijos stalčių + Siųsti priedą + + %d naudotojas perskaitė + %d naudotojai perskaitė + %d naudotojų perskaitė + + Failas yra per didelis, kad jį būtų galima įkelti. + Pridėti paveikslėlį iš + Šis turinys buvo praneštas kaip nepadorus. +\n +\nJei nenorite matyti daugiau šio naudotojo turinio, galite jį ignoruoti kad paslėpti jo žinutes. + Pranešta kaip nepadorus turinys + Apie šį turinį buvo pranešta kaip apie šlamštą. +\n +\nJei nenorite matyti daugiau šio naudotojo turinio, galite jį ignoruoti kad paslėpti jo žinutes. + Pranešta kaip šlamštas + Buvo pranešta apie šį turinį. +\n +\nJei nenorite matyti daugiau šio naudotojo turinio, galite jį ignoruoti kad paslėpti jo žinutes. + Turinys praneštas + IGNORUOTI NAUDOTOJĄ + PRANEŠTI + Pranešimo apie šį turinį priežastis + Pranešti apie šį turinį + Pasirinktinis pranešimas… + Tai nepadoru + Tai šlamštas + Šiame kambaryje nėra failų + %1$s %2$s + FAILAI + Šiame kambaryje nėra medijos + MEDIJA + %1$d iš %2$d + Nepavyko tvarkyti bendrinimo duomenų + Pasukti ir apkarpyti + Vietovė + Apklausa + Lipdukas + Galerija + Kamera + Kontaktas + Failas + Įveskite raktažodžius, reakcijos radimui. + Spoileris + Siunčia duotą žinutę kaip spoilerį + Nepadarėte jokių pakeitimų + %1$s nepadarė jokių pakeitimų + %1$s padarė šį kambarį tik pakviestiems. + Paviešinote kambarį visiems, kurie žino nuorodą. + %1$s paviešino kambarį visiems, kurie žino nuorodą. + Ilgai spauskite ant kambario, kad pamatytumėte daugiau parinkčių + Jūs neignoruojate jokių naudotojų + Pašalinti iš žemo prioriteto + Pridėti prie žemo prioriteto + Pašalinti iš parankinių + Pridėti prie parankinių + Ignoruoti naudotoją + Visos žinutės (triukšmingas) + Nutildyti + Tik paminėjimai + Visos žinutės + Nustatymai + Kambario nustatymai + Išeiti iš kambario + Padarėte šitai tik pakviestiems. + %1$s padarė šitai tik pakviestiems. + Padarėte šį kambarį tik pakviestiems. + Žinučių siuntimas jūsų komandai. + Saugus žinučių siuntimas. + Jūs viską kontroliuojate. + Turėkite savo pokalbius. + Neperskaitytos žinutės + Dar nesate tikri\? %s + Bendruomenės + Komandos + Redaguoti + Arba + Kur laikomi jūsų pokalbiai + Kur bus laikomi jūsų pokalbiai + Turi būti ne mažiau kaip 8 simboliai + Kiti gali jus atrasti %s + Sukurti savo paskyrą + Jūsų paskyra %s buvo sukurta + Sveikiname! + Pasiimkite mane namo + Suasmeninti profilį + Prisijungti prie serverio + Norite prisijungti prie esamo serverio\? + Praleisti šį klausimą + Sveiki sugrįžę! + Perskaitykite %s sąlygas ir taisykles + Serverio politikos + Patikrinkite savo el. paštą. + Susisiekite su mumis + Element Matrix Services (EMS) yra tvirta ir patikima talpinimo paslauga, skirta greitam ir saugiam bendravimui realiuoju laiku. Sužinokite, kaip <a href=\"${ftue_ems_url}\">element.io/ems</a> + Norite turėti savo serverį\? + %s atsiųs jums patvirtinimo nuorodą + Serverio URL + Patvirtinimo kodas + Koks yra jūsų serverio adresas\? + Koks yra jūsų serverio adresas\? Tai tarsi visų jūsų duomenų namai + Pasirinkti savo serverį + Telefono numeris + %s turi patvirtinti jūsų paskyrą + Įveskite savo telefono numerį + El. paštas + %s turi patvirtinti jūsų paskyrą + Įveskite savo el. paštą + Įsitikinkite, kad jis yra 8 ar daugiau simbolių. + Pasirinkite naują slaptažodį + Naujas slaptažodis + Pranešimų tikslai + olm versija + Naudokite integracijų tvarkyklę botams, tiltams, valdikliams ir lipdukų paketams tvarkyti. +\nIntegracijų valdytojai gauna konfigūracijos duomenis ir gali keisti valdiklius, siųsti kvietimus į kambarius ir nustatyti galios lygius jūsų vardu. + Telefonų knygos šalis + Vietiniai kontaktai + Prisegti kambarius su praleistais pranešimais + Pradžios ekranas + Nuorodų peržiūra pokalbyje, kai jūsų namų serveris palaiko šią funkciją. + Įterptinė URL peržiūra + Prisegti kambarius su neperskaitytomis žinutėmis + Integracijos + Kriptografijos raktų valdymas + Kriptografija + Padėkite mums nustatyti problemas ir tobulinti ${app_name} dalydamiesi anoniminiais naudojimo duomenimis. Kad suprastume, kaip žmonės naudojasi keliais įrenginiais, sugeneruosime atsitiktinį identifikatorių, kuriuo dalijasi jūsų įrenginiai. +\n +\nGalite perskaityti visas mūsų sąlygas %s. + Jei įjungta, kitiems naudotojams visada atrodysite neprisijungę, net jei naudosite programą. + Neprisijungęs režimas + Esamumas + Amžinai + 1 mėnuo + 1 savaitė + 3 dienos + Groti užrakto garsą + Pasirinkti + Numatytasis medijos šaltinis + Pasirinkti + Numatytasis glaudinimas + Medija + Pasirinkti šalį + Sutikote siųsti el. paštus ir telefono numerius į šį tapatybės serverį, kad būtų galima atrasti kitus naudotojus iš jūsų kontaktų. + Siųsti el. paštus ir telefono numerius į %s + Duoti sutikimą + Atšaukti mano sutikimą + Jūsų kontaktai yra privatūs. Kad galėtume rasti naudotojus iš jūsų kontaktų, mums reikia jūsų leidimo siųsti kontaktinę informaciją į jūsų tapatybės serverį. + Išsiuntėme jums patvirtinimo el. laišką į %s, pirmiausia patikrinkite savo el. paštą ir spustelėkite patvirtinimo nuorodą + Išsiuntėme jums patvirtinimo el. laišką į %s, patikrinkite savo el. paštą ir spustelėkite patvirtinimo nuorodą + Atrandami telefono numeriai + Atsijungimas nuo tapatybės serverio reiškia, kad jūsų negalės rasti kiti naudotojai ir negalėsite pakviesti kitų el. paštu ar telefonu. + Pridėjus telefono numerį bus rodomos atradimo parinktys. + Pridėjus el. pašto adresą, bus rodomos atradimo parinktys. + Atrandami el. pašto adresai + Šiuo metu nenaudojate tapatybės serverio. Norėdami atrasti esamus žinomus kontaktus ir būti jų atrandami, sukonfigūruokite jį žemiau. + Šiuo metu naudojate %1$s, esamų kontaktų atradimui, kuriuos pažįstate, ir kad būtumėte jų atrandami. + Tapatybės serveris nepateikė jokios politikos + BETA + Temos yra nebaigtas darbas, kuriame bus naujų, įdomių būsimų funkcijų, pvz., patobulinti pranešimai. Norėtume išgirsti jūsų atsiliepimus! + Temų Beta atsiliepimai + Tvarkyti el. paštus ir telefono numerius susietus su jūsų Matrix paskyra + El. paštai ir telefono numeriai + Rodyti visas žinutes nuo %s\? + Jūsų slaptažodis buvo atnaujintas + Slaptažodis nėra tinkamas + Nepavyko atnaujinti slaptažodžio + Naujas slaptažodis + Dabartinis slaptažodis + Keisti slaptažodį + Slaptažodis + Šis telefono numeris jau naudojamas. + Šis el. pašto adresas jau naudojamas. + Patikrinkite savo el. paštą ir spustelėkite jame esančią nuorodą. Kai tai padarysite, spauskite tęsti. + Padėkite tobulinti ${app_name} + ${app_name} renka anoniminę analizę, kad galėtume tobulinti programą. + Pasirinkti kalbą + Kalba + Siųsti analitikos duomenis + Analitika + Tvarkyti atradimo nustatymus. + Atradimas + Deaktyvuoti mano paskyrą + Tai pakeis dabartinį raktą arba frazę. + Generuoti naują saugumo raktą arba nustatyti naują esamos atsarginės kopijos saugumo frazę. + Apsisaugokite nuo užšifruotų žinučių ir duomenų praradimo, darydami šifravimo raktų atsargines kopijas serveryje. + Nustatyti šiame įrenginyje + Nustatyti saugią atsarginę kopiją iš naujo + Nustatyti saugią atsarginę kopiją + Saugi atsarginė kopija + Pridėti žinutės kompozitoriuje mygtuką jaustukų klaviatūros atidarymui + Rodyti jaustukų klaviatūrą + Programinės klaviatūros mygtukas Enter išsiųs žinutę, o ne pridės eilutės pertrauką + Siųsti žinutę su enter + Medijos peržiūra prieš siunčiant + Vibruoti paminėjus naudotoją + Įtraukiami avataro ir rodomojo vardo keitimai. + Rodyti paskyrų įvykius + Kvietimai, pašalinimai ir užblokavimai nėra įtakojami. + Rodyti prisijungimo ir išėjimo įvykius + Paleisti animuotus paveikslėlius laiko juostoje, kai tik jie tampa matomi + Automatinis animuotų vaizdų paleidimas + Naudokite /confetti komandą arba siųskite žinutę, kurioje yra ❄️ arba 🎉 + Rodyti pokalbio efektus + Spustelėkite ant skaitymo kvitų, kad pamatytumėte išsamų sąrašą. + Rodyti skaitymo kvitus + Rodyti laiko žymas 12 valandų formatu + Leidimas naudotis kontaktais + Rodyti laiko žymas visoms žinutėms + Prieš siunčiant žinutes, suformatuoti jas naudojant Markdown sintakse. Tai leidžia atlikti išplėstinį formatavimą, pavyzdžiui, naudoti žvaigždutes tekstui kursyvu rodyti. + Markdown formatavimas + Naudotojo sąsaja + Leisti kitiems naudotojams žinoti, kad rašote. + Norėdami tai daryti, Įjunkite \'Leisti integracijas\' nustatymuose. + Siųsti pranešimus apie rašymą + Trečiųjų šalių bibliotekos + Jūsų tapatybės serverio politika + Jūsų namų serverio politika + ${app_name} politika + Integracijų tvarkyklė + Leisti integracijas + Tapatybės serveris + Namų serveris + Prisijungta kaip + Autentifikacija + %1$s @ %2$s + Paskutinį kartą matytas + Atnaujinti viešą pavadinimą + Viešas pavadinimas + Deaktyvuoti paskyrą + ID + Tai galite bet kada išjungti nustatymuose + Mes <b>nesidalijame</b> informacija su trečiosiomis šalimis + Mes <b>neįrašome ir neprofiliuojame</b> jokių paskyros duomenų + čia + Integracijos yra išjungtos + Šis serveris nepateikia jokios politikos. + Išsiuntėte duomenis skambučiui nustatyti. + Slėpti tapatybės serverio politiką + Rodyti tapatybės serverio politiką + Failas %1$s buvo atsiųstas! + Suglaudinamas vaizdo įrašas %d%% + Suglaudinamas paveikslėlis… + Siunčiamas failas (%1$s / %2$s) + Siunčiama miniatiūra (%1$s / %2$s) + Užšifruojamas failas… + Užšifruojama miniatiūra… + Nerandate to, ko ieškote\? + Laukiama… + Filtruoti pokalbius… + Redagavimų nerasta + Žinutės redagavimai + (redaguota) + Pagrindiniame ekrane pridėti specialų skirtuką neperskaitytiems pranešimams. + Įjungti perbraukimą, kad atsakytumėte laiko juostoje + Ieškoti pavadinimo + Ieškoti pagal vardą, ID arba paštą + Pavadinimas arba ID (#pavyzdys:matrix.org) + Peržiūrėti kambarių katalogą + Siųsti naują tiesioginę žinutę + Tiesioginės žinutės + Sukurti naują kambarį + Pasiūlymai + Žinomi naudotojai + Kuriamas kambarys… + QR kodas + Pridėti pagal QR kodą + Būkite atrandami kitų + Paslaugų teikimo sąlygos + Peržiūrėti redagavimo istoriją + Nuoroda nukopijuota į iškarpinę + Atidaryti atradimo nustatymus + Rodyti pilną istoriją užšifruotuose kambariuose + Rodyti paslėptus įvykius laiko juostoje + Iš naujo nustatyti pranešimų metodą + Registruoti žetoną + Sistemos nustatymai + Nėra registruotų tiesioginių pranešimų vartų + Nėra nustatytų tiesioginų pranešimų taisyklių + Tiesioginių pranešimų taisyklės + Saugumas & Privatumas + Nuostatos + Bendrieji + Kiti trečiųjų šalių pranešimai + Matrix SDK versija + Kambario nustatymai + Rodyti pašalintų žinučių vietoje užrašą + Rodyti pašalintas žinutes + ištrinti iš serverio atsarginę šifravimo raktų kopiją\? Atkūrimo rakto nebegalėsite naudoti užšifruotai žinučių istorijai skaityti. + Ištrinti atsarginę kopiją + Tikrinama atsarginės kopijos būsena + Atsarginė kopija ištrinama… + Jei norite naudoti atsarginę raktų kopiją šioje sesijoje, dabar atkurkite naudodami slaptažodį arba atkūrimo raktą. + Atsarginė kopija turi netinkamą parašą iš nepatvirtintos sesijos %s + Atsarginė kopija turi netinkamą parašą iš patvirtintos sesijos %s + Įjungti sistemos kamerą, vietoj pritaikytos kameros ekrano. + Naudoti vietinę kamerą + Patvirtinkite palygindami šiuos duomenis su naudotojo nustatymais kitoje sesijoje: + Tvarkyti raktų atsarginę kopiją + Tema + Atšaukti nustatymą pagrindiniu adresu + Nustatyti kaip pagrindinį adresą + Tai eksperimentinės funkcijos, kurios gali netikėtai sugesti. Naudokite atsargiai. + Laboratorijos + Kambario versija + Šio kambario vidinis ID + Išplėstiniai + + %d užblokuotas naudotojas + %d užblokuoti naudotojai + %d užblokuotų naudotojų + + Užblokuoti naudotojai + Bet kas gali rasti kambarį ir prisijungti + Viešas + Tik pakviesti žmonės gali rasti ir prisijungti + Privatus (tik su kvietimais) + Privatus + Nežinomas prieigos nustatymas (%s) + Bet kas gali pasibelsti į kambarį, o nariai gali priimti arba atmesti + Tik nariai (nuo jų prisijungimo) + Tik nariai (nuo jų pakvietimo) + Tik nariai (nuo šios parinkties pasirinkimo momento) + Bet kas + Leisti svečiams prisijungti + Pranešti man apie + Peržiūrėti ir tvarkyti šio kambario adresus bei jo matomumą kambarių kataloge. + Kas gali prieiti\? + Pakeitimai, kas gali skaityti istoriją, bus taikomi tik būsimoms šio kambario žinutėms. Esamos istorijos matomumas išliks nepakitęs. + Kas gali skaityti istoriją\? + Kambario istorijos skaitomumas + Paskyros nustatymai + Tema + Kambario adresai + Kambario prieiga + Pranešimus galite tvarkyti %1$s. + Atkreipkite dėmesį, kad pranešimai apie paminėjimus ir raktinius žodžius užšifruotuose kambariuose, nėra prieinami mobiliuosiuose įrenginiuose. + Pranešimų konfigūracija + Įjungus šį nustatymą, prie visų veiksmų pridedamas žymuo FLAG_SECURE. Iš naujo paleiskite programą, kad pakeitimas įsigaliotų. + Neleisti programos ekrano nuotraukų + Biometrinis autentifikavimas buvo išjungtas, nes neseniai buvo pridėtas naujas biometrinis autentifikavimo metodas. Jį vėl galite įjungti nustatymuose. + Nepavyko įjungti biometrinio autentifikavimo. + Atidaryti nustatymus + Sukurti AŽ tik po pirmos žinutės + Įjungti atidėtas AŽ + Supaprastintas Element su nebūtinais skirtukais + Įjungti naują išdėstymą \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index f1fa1502c1..1787653fae 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -469,7 +469,7 @@ Tēma Atšifrēšanas kļūda Ierīces nosaukums - Sesijas ID + Sesijas ID Sesijas atslēga Eksportēt istabas šifrēšanas atslēgas Eksportēt istabas atslēgas diff --git a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml index 031b380c7e..7af718d920 100644 --- a/library/ui-strings/src/main/res/values-nb-rNO/strings.xml +++ b/library/ui-strings/src/main/res/values-nb-rNO/strings.xml @@ -119,7 +119,7 @@ Bannlyste brukere Avansert Tema - Økt-ID + Økt-ID Øktnøkkel Eksporter Importer diff --git a/library/ui-strings/src/main/res/values-nl/strings.xml b/library/ui-strings/src/main/res/values-nl/strings.xml index b1d239963e..ce122b0646 100644 --- a/library/ui-strings/src/main/res/values-nl/strings.xml +++ b/library/ui-strings/src/main/res/values-nl/strings.xml @@ -3,10 +3,10 @@ Uitnodiging van %s %1$s heeft %2$s uitgenodigd %1$s heeft u uitgenodigd - %1$s neemt nu deel aan het gesprek - %1$s heeft het gesprek verlaten + %1$s is deelnemer geworden van de kamer + %1$s heeft het de kamer verlaten %1$s heeft de uitnodiging geweigerd - %1$s heeft %2$s uit het gesprek verwijderd + %1$s heeft %2$s verwijderd %1$s heeft %2$s ontbannen %1$s heeft %2$s verbannen %1$s heeft de uitnodiging van %2$s ingetrokken @@ -15,20 +15,20 @@ %1$s heeft zijn/haar naam aangepast van %2$s naar %3$s %1$s heeft zijn/haar naam verwijderd (%2$s) %1$s heeft het onderwerp veranderd naar: %2$s - %1$s heeft de gespreksnaam veranderd naar: %2$s + %1$s heeft de kamernaam veranderd naar: %2$s %s heeft een video-oproep gemaakt. %s heeft een spraakoproep gemaakt. %s heeft de oproep beantwoord. %s heeft de oproep beëindigd. - %1$s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %2$s - alle deelnemers aan het gesprek, vanaf het punt dat ze zijn uitgenodigd. - alle deelnemers aan het gesprek, vanaf het punt dat ze zijn toegetreden. - alle deelnemers aan het gesprek. + %1$s heeft de toekomstige kamergeschiedenis zichtbaar gemaakt voor %2$s + alle kamerdeelnemers, vanaf het punt dat ze zijn uitgenodigd. + alle kamerdeelnemers, vanaf het punt dat ze deelnemer zijn geworden. + alle kamerdeelnemers. iedereen. (avatar is ook veranderd) - %1$s heeft de gespreksnaam verwijderd - %1$s heeft het gespreksonderwerp verwijderd - %1$s heeft een uitnodiging naar %2$s gestuurd om het gesprek toe te treden + %1$s heeft de kamernaam verwijderd + %1$s heeft het kameronderwerp verwijderd + %1$s heeft een uitnodiging naar %2$s gestuurd om deelnemer te worden van de kamer %1$s heeft de uitnodiging voor %2$s aanvaard ** Kan niet ontsleutelen: %s ** Het apparaat van de afzender heeft geen sleutels voor dit bericht gestuurd. @@ -36,27 +36,27 @@ Matrix-fout E-mailadres Telefoonnummer - Gespreksuitnodiging + Kameruitnodiging %1$s en %2$s - Leeg gesprek + Lege kamer Initiële synchronisatie: \nAccount wordt geïmporteerd… Initiële synchronisatie: \nCrypto wordt geïmporteerd Initiële synchronisatie: -\nGesprekken worden geïmporteerd +\nKamers importeren Initiële synchronisatie: -\nDeelgenomen gesprekken worden geïmporteerd -\nDit kan enige tijd in beslag nemen +\nGesprekken worden geladen +\nAls u aan veel kamers deelneemt kan dit even duren Initiële synchronisatie: -\nUitgenodigde gesprekken worden geïmporteerd +\nUitgenodigde kamers worden geïmporteerd Initiële synchronisatie: -\nVerlaten gesprekken worden geïmporteerd +\nVerlaten kamers worden geïmporteerd Initiële synchronisatie: \nAccountgegevens worden geïmporteerd - %s heeft dit gesprek geüpgraded. + %s heeft deze kamer geüpgraded. Bericht wordt verstuurd… - %1$s heeft de uitnodiging voor %2$s om het gesprek toe te treden ingetrokken + %1$s heeft de uitnodiging voor %2$s om deelnemer te worden van de kamer ingetrokken Uitnodiging van %1$s. Reden: %2$s %1$s heeft %2$s uitgenodigd. Reden: %3$s %1$s heeft u uitgenodigd. Reden: %2$s @@ -69,8 +69,8 @@ %1$s heeft de uitnodiging voor %2$s aanvaard. Reden: %3$s %1$s heeft de uitnodiging van %2$s ingetrokken. Reden: %3$s - %1$s heeft %2$s als gespreksadres toegevoegd. - %1$s heeft %2$s als gespreksadressen toegevoegd. + %1$s heeft %2$s als kameradres toegevoegd. + %1$s heeft %2$s als kameradressen toegevoegd. %1$s heeft %2$s als gespreksadres verwijderd. @@ -123,7 +123,7 @@ Logboek versturen Crash-logboek versturen Schermafdruk versturen - Fout melden + Probleem melden Beschrijf de fout. Wat heeft u gedaan\? Wat verwachtte u dat er zou gebeuren\? Wat is er echt gebeurd\? Beschrijf hier uw probleem Om het probleem te kunnen onderzoeken worden logboeken van deze cliënt met de foutmelding verstuurd. Deze foutmelding, inclusief de logboeken en schermafdruk, zullen niet openbaar zichtbaar zijn. Indien u liever alleen de bovenstaande tekst verstuurt, haal dan het vinkje weg: @@ -132,7 +132,7 @@ Versturen van foutmelding is mislukt (%s) Voortgang (%s%%) De toepassing is de vorige keer gecrasht. Wilt u dit melden\? - Gesprek toetreden + Deelnemen aan kamer Inlognaam Afmelden Server-URL @@ -172,9 +172,9 @@ NEE Verdergaan Verwijderen - Toetreden + Deelnemen Afwijzen - Ga naar ongelezen + Naar ongelezen springen Gesprek verlaten Weet u zeker dat u het gesprek wilt verlaten\? TWEEGESPREKKEN @@ -210,7 +210,7 @@ Telefoonnummer toevoegen Toon informatie over de app in de systeeminstellingen. App-informatie - Meldingen voor deze account inschakelen + Meldingen voor dit account inschakelen Meldingen voor deze sessie inschakelen Berichten in één-op-één-gesprekken Berichten in groepsgesprekken @@ -227,7 +227,7 @@ Copyright Privacybeleid Cache wissen - Persoonsinstellingen + Gebruikersinstellingen Meldingen Genegeerde personen Overige @@ -265,7 +265,7 @@ Iedereen Alleen deelnemers (vanaf het moment dat deze optie wordt geselecteerd) Alleen deelnemers (vanaf het moment dat ze worden uitgenodigd) - Alleen deelnemers (vanaf het moment dat ze toetreden) + Alleen deelnemers (vanaf het moment dat ze deelnemer zijn geworden) Verbannen personen Geavanceerd Interne ID van dit gesprek @@ -275,7 +275,7 @@ Niet instellen als hoofdadres Ontsleutelingsfout Publieke naam - Sessie ID + Sessie-ID Sessiesleutel E2E-gesprekssleutels exporteren Gesprekssleutels exporteren @@ -294,7 +294,7 @@ Verifiëren Om te verifiëren dat deze sessie vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar persoonsinstellingen van deze sessie overeenkomt met de sleutel hieronder: Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders deze sessie en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden. - Kies een gesprekscatalogus + Kamermap kiezen Servernaam Alle gesprekken op server %s Alle lokale gesprekken op %s @@ -334,7 +334,7 @@ user_id ontbreekt in het verzoek. Gesprek %s is niet zichtbaar. Matrix-apps toevoegen - Geluidsmeldingen + Belangrijke meldingen Stille meldingen Foutmelding Foto maken @@ -423,7 +423,7 @@ Voer uw wachtwoord in. Beschrijf het probleem in het Engels, indien mogelijk. Media bekijken vóór het versturen - Toont een actie + Geeft activiteit weer Verbant persoon met gegeven ID Heft verbanning van persoon met gegeven ID op Stel het machtsniveau van een persoon in @@ -456,7 +456,7 @@ Aanvaarden Gelieve het beleid van deze server te lezen en aanvaarden: Oproepen - Gebruik de standaardbeltoon van ${app_name} voor inkomende oproepen + Standaardbeltoon van ${app_name} gebruiken voor inkomende oproepen Beltoon voor inkomende oproepen Selecteer beltoon voor oproepen: Eruit sturen @@ -551,7 +551,7 @@ ${app_name} wordt niet beperkt door accuoptimalisatie. Als een persoon een apparaat los van de oplader een tijd laat stilliggen, met het scherm uitgeschakeld, gaat het apparaat in slaapmodus. Dit verhindert apps de toegang tot het netwerk, en stelt hun taken, synchronisaties en standaardalarmen uit. Optimalisatie negeren - Lawaaiierige meldingen configureren + Belangrijke meldingen configureren Oproepmeldingen configureren Stille meldingen configureren Bepaal de LED-kleur, vibratie, geluid, … @@ -722,7 +722,7 @@ Gebruik een integratiebeheerder om bots, bruggen, widgets en stickerpakketten te beheren. \nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, gespreksuitnodigingen versturen en bestuursniveaus instellen namens u. Ontdekken - Beheer uw ontdekinstellingen. + Beheer uw ontdekkingsinstellingen. Integraties toestaan Integratiebeheerder Widget @@ -774,18 +774,18 @@ Gebeurtenis verwijderd door persoon Gebeurtenis gemodereerd door gesprek beheerder Niet correcte gebeurtenis, kan niet weergeven - Maak een nieuw gesprek aan + Nieuwe kamer aanmaken Geen netwerk. Controleer uw internet verbinding. Wijzigen - Wijzig netwerk + Netwerk wijzigen Even wachten… - Dit gesprek kan niet worden voorvertoond + Deze kamer kan niet worden voorvertoond Gesprekken Directe Berichten AANMAKEN - Gespreksnaam + Naam Publiek - Iedereen kan deze kamer kunnen toetreden + Iedereen kan deelnemer worden van deze kamer Afspelen U heeft het hoofdadres voor dit gesprek verwijderd. U heeft %1$s uitgenodigd. Reden: %2$s @@ -861,8 +861,8 @@ U heeft een audiogesprek geopend. U heeft een videogesprek geopend. U heeft de kamernaam veranderd naar: %1$s - U heeft de kamer afbeelding aangepast - %1$s heeft de kamer afbeelding aangepast + U heeft de kamerafbeelding aangepast + %1$s heeft de kamerafbeelding aangepast U heeft het onderwerp gewijzigd naar: %1$s U heeft uw weergavenaam verwijderd (voorheen %1$s) U heeft de uitnodiging van %1$s ingetrokken @@ -891,8 +891,8 @@ U heeft %1$s als gespreksadressen verwijderd. - U heeft %1$s als gespreksadres toegevoegd. - U heeft %1$s als gespreksadressen toegevoegd. + U heeft %1$s als kameradres toegevoegd. + U heeft %1$s als kameradressen toegevoegd. U heeft de uitnodiging van %1$s ingetrokken. Reden: %2$s U heeft de uitnodiging voor %1$s aanvaard. Reden: %2$s @@ -913,12 +913,12 @@ • Servers die overeenkomen met %s zijn verbannen. U heeft hier geüpgraded. %s heeft hier geüpgraded. - U heeft toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %1$s + U heeft toekomstige kamergeschiedenis zichtbaar gemaakt voor %1$s %1$ds over %s is toegetreden. Conclusie Bevestiging Kamerinstellingen - Gespreksnaam + Kamernaam Integraties Beheren %d uitnodiging @@ -940,7 +940,7 @@ PIN Bevestigen Uitnodiging intrekken Contactpersonen - Gespreksnaam + Kamernaam Beveiligingszin Instellen Beveiligde backup @@ -962,7 +962,7 @@ Sleutelverzoeken Verwijderen Bevestigen Accountgegevens - Ontwikkel Gereedschap + Ontwikkelaarsgereedschap QR-code Sleutels herstellen Gekruist Ondertekenen Initialiseren @@ -971,7 +971,7 @@ Actieve Sessies Versleuteling inschakelen Versleuteling inschakelen\? - Berichtverwerker + Berichtbewerker Gesprek Verlaten Eén persoon @@ -1043,7 +1043,7 @@ Directe Berichten Feedback Token registreren - Pushregels + Push-regels Bericht verwijderd Beveiligde Backup Actieve widgets @@ -1061,7 +1061,7 @@ SSL-fout. Camera wisselen Draadloze Koptelefoon - Spaces + Ruimten Wisselen Opwaarderen Aanbevolen @@ -1187,7 +1187,7 @@ Kopiëren Geef toestemming om de camera te gebruiken via de systeeminstellingen om deze actie uit te voeren. Sommige rechten ontbreken om deze actie uit te voeren, geeft a.u.b. toestemming via de systeeminstellingen. - Spaces + Ruimten Begin met chatten Herstellen Afwijzen @@ -1224,13 +1224,13 @@ Aan de slag Spacerechten Gespreksrechten - Door deze persoon niet meer de verbannen kan hij/zij opnieuw toetreden tot de space. - Door deze persoon niet meer de verbannen kan hij/zij opnieuw toetreden tot het gesprek. + Door de verbanning op te heffen kan deze gebruiker opnieuw deelnemer worden van de ruimte. + Door de verbanning op te heffen kan deze gebruiker opnieuw deelnemer worden van de kamer. Door deze persoon te verbannen zal hij/zij verwijderd worden uit deze space en voorkomen dat hij/zij opnieuw toetreedt. Reden voor verbanning - Door deze persoon de verwijderen zal hij/zij niet meer in deze space zitten. + De gebruiker zal worden verwijderd uit deze ruimte. \n -\nOm te voorkomen dat hij/zij opnieuw toetreedt, kunt u hem/haar ook verbannen. +\nOm te voorkomen dat ze opnieuw toetreden, kunt u ze verbannen. Door deze persoon te verwijderen zal hij/zij niet meer in dit gesprek zitten. \n \nOm te voorkomen dat hij/zij opnieuw toetreedt, kun je hem/haar ook verbannen. @@ -1241,7 +1241,7 @@ \n \nU kunt deze actie op elk moment ongedaan maken in de algemene instellingen. U kunt deze wijziging niet ongedaan maken omdat uzelf degradeert, als u de laatste persoon met rechten bent in het gesprek zal het onmogelijk zijn om opnieuw rechten te krijgen. - Dit gesprek is niet publiek. U kunt niet opnieuw toetreden zonder uitnodiging. + Deze kamer is niet publiek. U kunt niet opnieuw deelnemer worden zonder uitnodiging. Toegang verlenen tot uw contactpersonen. Om de QR-code te scannen moet u toegang verlenen tot de camera. Oproep beëindigen… @@ -1353,7 +1353,7 @@ Toevoegen aan lage prioriteit Verwijder van favorieten Toevoegen aan favorieten - Alle berichten (luidruchtig) + Alle belangrijke berichten Deze inhoud is als ongepast gerapporteerd. \n \nAls u geen inhoud van deze persoon meer wilt zien, kunt u deze negeren om hun berichten te verbergen. @@ -1396,7 +1396,7 @@ Het lijkt erop dat de server er te lang over doet om te reageren. Dit kan worden veroorzaakt door een slechte verbinding of een fout met de server. Probeer het over een tijdje opnieuw. Probeer het opnieuw zodra u de algemene voorwaarden van uw homeserver hebt geaccepteerd. Uitgebreide logboeken helpen ontwikkelaars door meer logboeken te verstrekken wanneer u een RageShake verzendt. Zelfs wanneer ingeschakeld, registreert de toepassing geen berichtinhoud of andere privégegevens. - Uitgebreide logboeken inschakelen. + Uitgebreide logboeken inschakelen Ga akkoord met de servicevoorwaarden van de identiteitsserver (%s), zodat u vindbaar bent op e-mailadres of telefoonnummer. U deelt momenteel e-mailadressen of telefoonnummers op de identiteitsserver %1$s. U moet opnieuw verbinding maken met %2$s om ze niet meer te delen. De verificatiecode is niet correct. @@ -1411,7 +1411,7 @@ Stuur e-mailadressen en telefoonnummers naar %s Toestemming geven Mijn toestemming intrekken - Uw server-beleid + Uw thuisserverbeleid Kan geen server bereiken op de URL %s. Controleer uw link of kies handmatig een server. Uw contacten zijn privé. Om personen van uw contacten te ontdekken, hebben we uw toestemming nodig om contactgegevens naar uw identiteitsserver te sturen. We hebben u een bevestigingsmail gestuurd naar %s, controleer eerst uw e-mail en klik op de bevestigingslink @@ -1467,8 +1467,8 @@ Afbeelding comprimeren… Bestand versturen (%1$s / %2$s) Miniatuur versturen (%1$s / %2$s) - Toon volledige geschiedenis in versleutelde kamers - Toon verborgen gebeurtenissen op de tijdlijn + Volledige geschiedenis in versleutelde kamers weergeven + Verborgen gebeurtenissen op de tijdlijn weergeven Geef feedback De feedback kan niet worden verzonden (%s) Bedankt, uw feedback is succesvol verzonden @@ -1479,8 +1479,8 @@ Bedankt, de suggestie is succesvol verzonden Beschrijf hier uw suggestie Schrijf hieronder uw suggestie. - Doe een suggestie - Systeem instellingen + Een voorstel doen + Systeeminstellingen Versies Hulp bij het gebruik van ${app_name} Hulp en ondersteuning @@ -1500,9 +1500,9 @@ \n \n%s Kameronderwerp (optioneel) - Aanmaken nieuwe Space - Toon een aanduiding voor verwijderde berichten - Toon verwijderde berichten + Nieuwe ruimte aanmaken + Geeft een plaatsvervangende melding weer voor verwijderde berichten. + Verwijderde berichten weergeven Beveiligde back-up instellen Beveiliging tegen verlies van toegang tot versleutelde berichten en gegevens De herstelsleutel is opgeslagen. @@ -1565,7 +1565,7 @@ Gepubliceerde adressen kunnen door iedereen op elke server worden gebruikt om lid te worden van uw kamer. Om een adres te publiceren, moet het eerst als lokaal adres worden ingesteld. Gepubliceerde adressen Adressen van deze kamer bekijken en beheren. - Spaceadressen + Ruimte-adressen Bekijk en beheer de adressen van deze kamer en de zichtbaarheid ervan in de kamerdirectory. Kameradressen Sta toe om gasten te laten deelnemen @@ -1584,7 +1584,7 @@ Deze server biedt geen beleid. Bibliotheken van derden Uw identiteitsserverbeleid - ${app_name} beleid + ${app_name}-beleid We delen geen informatie met derden We registreren of profileren geen accountgegevens hier @@ -1601,7 +1601,7 @@ Voeg een knop toe aan de invoerveld om het emoji-toetsenbord te openen Emoji-toetsenbord weergeven Gebruik /confetti commando of stuur een bericht met ❄️ of 🎉 - Toon chateffecten + Chateffecten weergeven Kamer upgrades Berichten door bot Kameruitnodigingen @@ -1754,7 +1754,7 @@ Pincode is vereist na 2 minuten ${app_name} niet te hebben gebruikt. Pincode vereist na 2 minuten Geef alleen het aantal ongelezen berichten weer in een eenvoudige melding. - Toon details zoals kamernamen en berichtinhoud. + Geeft details weer zoals kamernamen en berichtinhoud. Inhoud in meldingen weergeven Pincode is de enige manier om ${app_name} te ontgrendelen. Schakel apparaatspecifieke biometrische gegevens in, zoals vingerafdrukken en gezichtsherkenning. @@ -1831,8 +1831,8 @@ Andere beschikbare talen Deel deze code met mensen zodat ze deze kunnen scannen om u toe te voegen en te beginnen met chatten. Mijn code - Deel mijn code - Scan een QR-code + Mijn code delen + Een QR-code scannen We kunnen geen personen uitnodigen. Controleer de personen die u wilt uitnodigen en probeer het opnieuw. Uitnodigingen verzonden naar %1$s en nog één @@ -1843,7 +1843,7 @@ Uitnodiging verzonden naar %1$s 🔐️ Doe mee met ${app_name} Hé, praat met me op ${app_name}: %s - Nodig vrienden uit + Vrienden uitnodigen Mensen toevoegen We kunnen je DM niet maken. Controleer de personen die u wilt uitnodigen en probeer het opnieuw. De link %1$s brengt u naar een andere site: %2$s. @@ -1912,7 +1912,7 @@ Kan sleutels niet importeren Wachten op %s… Bijna daar! Op bevestiging wachten… - Bijna daar! Toont het andere apparaat een vinkje\? + Bijna klaar! Toont het andere apparaat een vinkje\? Een onderwerp toevoegen %s om mensen te laten weten waar deze kamer over gaat. Dit is het begin van uw privéberichtgeschiedenis met %s. @@ -1989,7 +1989,7 @@ \nWees voorzichtig, het kan leiden tot onverwacht gedrag. Vliegtuigmodus is ingeschakeld Verbinding met de server is verbroken - Bijna daar! Toont %s een vinkje\? + Bijna klaar! Toont %s een vinkje\? Totdat deze persoon deze sessie vertrouwt, worden berichten die van en naar de sessie worden verzonden, gelabeld met waarschuwingen. U kunt het ook handmatig verifiëren. %1$s (%2$s) aangemeld met een nieuwe sessie: Deze sessie wordt vertrouwd voor veilig berichtenverkeer omdat %1$s (%2$s) deze heeft geverifieerd: @@ -2011,7 +2011,7 @@ Serverversie Server naam Afmelden voor deze sessie - Toon alle sessies + Alle sessies tonen Uw serverbeheerder heeft standaard end-to-end versleuteling uitgeschakeld in privékamers en privéberichten. Kruisondertekenen is niet ingeschakeld Kruisondertekenen is ingeschakeld. @@ -2070,21 +2070,21 @@ Ze komen niet overeen Niet-vertrouwd inloggen Uw e-maildomein is niet geautoriseerd om op deze server te registreren - Space aanmaken… + Ruimte aanmaken… Kamer aanmaken… Sommige tekens zijn niet toegestaan Geef een kameradres op Dit adres is al in gebruik - Space adres + Ruimte-adres U kunt dit inschakelen als de kamer alleen wordt gebruikt voor samenwerking met interne teams op uw server. Dit kan later niet meer worden gewijzigd. Blokkeer iedereen die geen deel uitmaakt van %s om ooit deel te nemen aan deze kamer Verberg geavanceerd - Toon geavanceerd + Geavanceerd weergeven Eenmaal ingeschakeld, kan versleuteling niet worden uitgeschakeld. Voegt ( ͡° ͜ʖ ͡°) toe aan een bericht in platte tekst Voegt ¯\\_(ツ)_/¯ toe aan een bericht in platte tekst - Toon wat nuttige informatie om te helpen bij het debuggen van de applicatie - Toon debug-informatie op het scherm + Geeft wat nuttige informatie weer om te helpen bij foutopsporing van de app. + Foutopsporingsinformatie op het scherm weergeven ${app_name} kan vaker crashen als er een onverwachte fout optreedt Laat alleen de eerste resultaten zien, typ meer letters… Schud je telefoon om de detectiedrempel te testen @@ -2157,9 +2157,9 @@ Ik heb mijn e-mailadres geverifieerd Tik op de link om uw nieuwe wachtwoord te bevestigen. Klik hieronder als u de link hebt gevolgd die erin staat. Word eigenaar van uw gesprekken. - Space aanmaken - Space aanmaken… - Aanmaken een space + Ruimte aanmaken + Ruimte aanmaken… + Een ruimte aanmaken Gebeurtenis inhoud Houd er rekening mee dat bij het upgraden een nieuwe versie van de kamer wordt gemaakt. Alle huidige berichten blijven in deze gearchiveerde kamer. Iedereen in een ouderkamer kan deze kamer vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamer instellingen. @@ -2247,7 +2247,7 @@ Experimenteel voelen\? \nU kunt bestaande spaces aan een space toevoegen. Alle kamers waarin u deelneemt, worden weergegeven in Home. - Toon alle kamers in Home + Alle kamers op startscherm weergeven Kamers en spaces beheren Markeren als aanbevolen Markeren als niet aanbevolen @@ -2300,16 +2300,16 @@ Voeg wat details toe om het te laten opvallen. U kunt deze op elk moment wijzigen. Alleen op uitnodiging, het beste voor uzelf of teams Open voor iedereen, het beste voor gemeenschappen - Een privé space voor u en uw teamgenoten + Een privé-ruimte voor u en uw teamgenoten Ik en teamgenoten Een privé space om je kamers te organiseren Alleen ik Zorg ervoor dat de juiste mensen toegang hebben tot %s. Met wie werkt u samen\? U kunt dit later wijzigen - Wat voor soort space wilt u aanmaken\? - Uw privé space - Uw openbare space + Wat voor soort ruimte wilt u aanmaken\? + Uw privé-ruimte + Uw openbare ruimte Space toevoegen Privé space Openbare space @@ -2352,7 +2352,7 @@ Locatie De versleuteling is verkeerd geconfigureerd, zodat u geen berichten kunt versturen. Klik om instellingen te openen. De versleuteling is verkeerd geconfigureerd, zodat u geen berichten kunt versturen. Neem contact op met een beheerder om de versleuteling in een geldige staat te herstellen. - Toon bericht bubbels + Berichtbubbels weergeven Kan kaart niet laden Kaart Let op: app wordt opnieuw gestart @@ -2393,7 +2393,7 @@ %1$d meer %1$d meer - Toon minder + Minder weergeven Locatie delen is bezig ${app_name} Live locatie Stop @@ -2471,8 +2471,8 @@ \n \nHoud er rekening mee dat deze actie de app opnieuw zal starten en dat dit enige tijd kan duren. Initieel synchronisatieverzoek - Toon de laatste profielinformatie (avatar en weergavenaam) voor alle berichten. - Toon laatste persoonsinformatie + Geeft de meest recente gebruikersinfo (avatar en weergavenaam) weer voor alle berichten. + Meest recente gebruikersinfo weergeven Bezet Back-up heeft een geldige handtekening van deze persoon. %1$s geleden bijgewerkt @@ -2563,7 +2563,7 @@ %s moet uw account verifiëren Vul uw e-mailadres in Lees de voorwaarden en het beleid van %s door - Serverbeleid + Serverbeleiden Neem contact op Element Matrix Services (EMS) is een robuuste en betrouwbare hostingservice voor snelle, veilige en realtime communicatie. Ontdek hoe op element.io/ems Wilt u uw eigen server hosten\? @@ -2596,15 +2596,15 @@ %1$s en %2$s E-mailadres niet geverifieerd, controleer je inbox - Toon alle sessies (V2, WIP) + Alle sessies weergeven (V2, WIP) Kan kaart niet laden \nDeze server is mogelijk niet geconfigureerd om kaarten weer te geven. Open instellingen - Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt. - Andere sessies + Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt. + Andere sessies Sessies Lijst met publieke spaces - Maak een nieuw gesprek of een nieuwe kamer + Nieuw gesprek of nieuwe kamer aanmaken Personen Favorieten Ongelezen @@ -2613,10 +2613,59 @@ Activiteit Sorteer op Recente tonen - Toon filters + Filters weergeven Lay-outvoorkeuren Ontdek kamers Kamer creëren - Start gesprek + Gesprek starten Alle gesprekken + U kunt feedback geven via het menu rechtsboven. + Krijg sneller en gemakkelijker toegang tot uw ruimten (rechtsonder). + Om ${app_name} te versimpelen zijn tabbladen nu optioneel. U kunt ze beheren in het menu rechtsboven. + Hier zullen uw ongelezen berichten verschijnen wanneer u deze heeft. + De allesomvattende beveiligde chat-app voor teams, vrienden en organisaties. Maak een gesprek aan of word deelnemer van een bestaande kamer om te beginnen. + Ruimten zijn een nieuwe manier om kamers en personen te groeperen. Voeg een bestaande kamer toe, of maak een nieuwe aan via de knop rechtsonder. + + Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke u niet meer gebruikt. + Overweeg uit te loggen van oude sessies (%1$d of meer dagen) welke u niet meer gebruikt. + + Verifieer of log uit van ongeverifieerde sessies. + Verbeter uw accountbeveiliging door deze aanbevelingen te volgen. + + Al %1$d+ dag inactief (%2$s) + Al %1$d+ dagen inactief (%2$s) + + Beveiligingsaanbevelingen + Niet-geverifieerde sessies + Inactieve sessies + %s +\nziet er vrij leeg uit. + Welkom bij ${app_name}, +\n%s. + Niets te melden. + Welkom bij een nieuw uiterlijk! + Toegang tot ruimten + Feedback geven + Uitproberen + Niet-geverifieerd · Laatste activiteit %1$s + Geverifieerd · Laatste activiteit %1$s + Alle bekijken (%1$d) + Details bekijken + Sessie verifiëren + Sorry, deze kamer kon niet worden gevonden. +\nProbeer het later opnieuw. %s + Dit is waar uw nieuwe verzoeken en uitnodigingen zullen verschijnen. + Ruimten zijn een nieuwe manier om kamers en personen te groeperen. Maak een ruimte aan om te beginnen. + Ongeverifieerde sessie + Geverifieerde sessie + Onbekend apparaattype + Desktop + Web + Mobiel + Niets nieuws. + Uitnodigingen + Nog geen ruimten. + %s subitems inklappen + %s subitems uitvouwen + Ruimte aanpassen diff --git a/library/ui-strings/src/main/res/values-nn/strings.xml b/library/ui-strings/src/main/res/values-nn/strings.xml index a56ba0ac30..45c8679736 100644 --- a/library/ui-strings/src/main/res/values-nn/strings.xml +++ b/library/ui-strings/src/main/res/values-nn/strings.xml @@ -310,7 +310,7 @@ Preg Noko gjekk gale med dekrypteringa Offentleg namn - Økt-ID + Økt-ID Sesjonsnøkkel Eksporter E2E-romnøkklar Eksporter romnøkklar diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index 18b0de078c..c9bac8977b 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -145,7 +145,7 @@ Przejdź do pierwszej nieprzeczytanej wiadomości Opuść pokój Czy na pewno chcesz opuścić pokój? - WIADOMOŚCI BEZPOŚREDNIE + Wiadomości bezpośrednie Zaproś Blokuj Odbanuj @@ -231,7 +231,7 @@ Ustaw jako główny adres Motyw Nazwa publiczna - ID sesji + ID sesji Eksportuj Wprowadź hasło Potwierdź hasło @@ -548,7 +548,7 @@ Pokoje Dodaj reakcję Utwórz nowy pokój - Wiadomości Bezpośrednie + Wiadomości bezpośrednie STWÓRZ Nazwa Publiczny @@ -654,7 +654,7 @@ Proszę wykonać kopię Preferencje Głos i wideo - Wiadomości Bezpośrednie + Wiadomości bezpośrednie Filtruj rozmowy… Wyślij nową wiadomość bezpośrednią Wersja Matrix SDK @@ -732,7 +732,7 @@ Wysyłaj wiadomości za pomocą klawisza enter Przycisk enter na klawiaturze programowej wyśle wiadomość zamiast wprowadzania łamanania linii Ustawienia wyszukiwania - Ustal jak inni mogą odnaleść twoje konto. + Ustal jak inni mogą odnaleźć twoje konto. Media Domyślne źródło mediów Odzyskiwanie zaszyfrowanych wiadomości @@ -841,7 +841,7 @@ Url: Format: Zarejestruj token - Dziękujemy, sugestia została szczęśliwie wysłana + Dziękujemy, sugestia została pomyślnie wysłana Wysłanie sugestii nie powiodło się (%s) Wyświetl ukryte wydarzenia na linii czasowej (edytowano) @@ -2697,8 +2697,8 @@ Nie można wczytać mapy. \nTen serwer macierzysty może nie być skonfigurowany do wyświetlania map. Otwórz ustawienia - Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz. - Inne sesje + Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz. + Inne sesje Sesje Lista otwartych przestrzeni Utwórz nową rozmowę lub pokój @@ -2721,10 +2721,8 @@ Nie zweryfikowano · Ostatnia aktywność %1$s Zweryfikowano · Ostatnia aktywność %1$s Pokaż wszystkie (%1$d) - Obecna sesja Pokaż szczegóły Zweryfikuj sesję - Twoja obecna sesja jest przygotowana do bezpiecznej komunikacji. Niezweryfikowana sesja Zweryfikowana sesja Nieznany typ urządzenia @@ -2734,4 +2732,16 @@ Niestety, ten pokój nie został znaleziony. \nSpróbuj ponownie później.%s Zaproszenia + Tutaj pojawią się rozmowy które nie zostały jeszcze odczytane. + Brak nowych wiadomości. + Zmień przestrzeń + Stwórz prywatny chat dopiero po wysłaniu pierwszej wiadomości + Włącz odroczone prywatne chaty + Odświeżony wygląd Element z opcjonalnymi kartami + Włącz nowy układ + Przestrzenie to nowa metoda na grupowanie razem wielu pokoi i osób. Dodaj tu już istniejący pokój lub stwórz nowy używając przycisku w prawym-dolnym rogu. + Jest to nowa metoda na grupowanie razem wielu pokoi i osób. + %s +\nwygląda nieco pusto. + Brak przestrzeni. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 08c41db365..108ecc7e38 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -1,6 +1,6 @@ - convite de %s + Convite de %s %1$s convidou %2$s %1$s convidou você %1$s juntou-se à sala @@ -418,7 +418,7 @@ Des-definir como endereço principal Erro de decriptação Nome público - ID de sessão + ID de sessão Chave de sessão Exportar chaves de sala E2E Exportar chaves de sala @@ -473,7 +473,7 @@ Você tem certeza que você quer começar uma chamada de vídeo\? Tirar foto Tirar vídeo - Chamar + Chamada Banir usuária(o) vai removê-la(o) desta sala e preveni-la(o) de se juntar de novo. Todas as mensagens Adicionar a tela de Início @@ -2320,7 +2320,7 @@ Enviar imagens e vídeos Abrir câmera Seu sistema vai automaticamente enviar logs quando um erro incapaz de decriptar ocorre - Auro Reportar Erros de Decriptação. + Auto Reportar Erros de Decriptação. Sobrepor cor de nome de exibição Eu já tenho uma conta Mensageria segura. @@ -2460,7 +2460,7 @@ 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í + Experimentar Compartilhamento de tela está em progresso ${app_name} Compartilhamento de Tela Parar compartilhamento de tela @@ -2477,7 +2477,7 @@ Backup tem uma assinatura válida desta(e) usuária(o). Atualizada %1$s atrás Implementação tempoária: locais persistem em histórico de sala - Habilitar Compartilhament de Localização Ao Vivo + Habilitar Compartilhamento de Localização Ao Vivo %1$s restando Ao vivo até %1$s Ver localização ao vivo @@ -2499,7 +2499,7 @@ Endpoint Gateway Ativar compartilhamento de localização - Compartilhamento de localização em tempo real + Compartilhamento de localização ao vivo Gateway atual: %s Não foi possível encontrar o endpoint. Endpoint atual: %s @@ -2538,7 +2538,7 @@ Nome de Usuária(o) / Email / Telefone Você é um/uma humano(a)\? Siga as instruções enviadas para %s - Reset de senha + Senha resettada Esqueceu senha Reenviar email Não recebeu um email\? @@ -2601,8 +2601,8 @@ Abrir configurações Todos os Chats Mostrar Todas Sessões (V2, WIP) - Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais. - Outras sessões + Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais. + Outras sessões Sessões Abrir lista de espaços Criar uma nova conversa ou sala @@ -2622,11 +2622,8 @@ Não-verificada · Última atividade %1$s Verificada · Última atividade %1$s Ver Todas (%1$d) - Sessão Atual Visualizar Detalhes Verificar Sessão - Verifique sua sessão atual para mensageria segura melhorada. - Sua sessão atual está pronta para mensageria segura. Sessão não-verificada Sessão verificada Tipo de dispositivo desconhecido @@ -2636,4 +2633,81 @@ Desculpe, esta sala não tem sido encontrada. \nPor favor retente mais tarde.%s Convites + Experimentar + Toque na direita topo para ver a opção para feedback. + Dê Feedback + Acesse seus Espaços (direita fundo) mais rápido e fácil que jamais antes. + Acesse Espaços + Para simplificar seu ${app_name}, abas são agora opcionais. Gerencie-as usando o menu direito topo. + Boas-vindas a uma nova visão! + Isto é onde suas mensagens não-lidas vão aparecer, quando você tiver algumas. + Nada para reportar. + O app de chat seguro tudo-em-um para equipes, amigas(os) e organizações. Crie um chat, ou junte-se a uma sala existe, para começar. + Boas-vindas a ${app_name}, +\n%s. + Espaços são uma nova maneira de agrupar salas e pessoas. Adicione uma sala existente, ou crie uma nova, usando o botão direito fundo. + %s +\nestá parecendo um pouco vazio. + + Considere fazer signout de sessões antigas (%1$d dia ou mais) que você não usa mais. + Considere fazer signout de sessões antigas (%1$d dias ou mais) que você não usa mais. + + Sessões inativas + Verificar ou fazer signout de sessões não-verificadas. + Sessões não-verificadas + Melhore a segurança de sua conta ao seguir estas recomendações. + Recomendações de segurança + + Inativa por %1$d+ dia (%2$s) + Inativa por %1$d+ dias (%2$s) + + Isto é onde suas novas requisições e convites vão estar. + Nada novo. + Espaços são uma nova maneira de agrupar salas e pessoas. Crie um espaço para começar. + Nenhum espaço ainda. + Colapsar filhos de %s + Expandir filhos de %s + Mudar Espaço + Não-verificadas + Verificadas + Não-verificadas + Verificadas + Inativas + + Inativas por %1$d dia ou mais longo + Inativas por %1$d dias ou mais longo + + Inativas + Endereço de IP + Última atividade + Nome de sessão + Informação de aplicativo, dispositivo, e atividade. + Detalhes de sessão + Limpar Filtro + Nenhuma sessão inativa encontrada. + Nenhuma sessão não-verificada encontrada. + Nenhuma sessão verificada encontrada. + + Considere fazer signout de sessões antigas (%1$d dia ou mais) que você não usa mais. + Considere fazer signout de sessões antigas (%1$d dias ou mais) que você não usa mais. + + Verifique suas sessões para mensageria segura melhorada ou faça signout daquelas que você não reconhece ou usa mais. + Para melhor segurança, faça signout de qualquer sessão que você não reconhece ou usa mais. + Filtrar + Pronta para mensageria segura + Não pronta para mensageria segura + Todas as sessões + Filtrar + Última atividade %1$s + Dispositivo + Sessão + Sessão Atual + Verifique ou faça signout desta sessão para melhor segurança e fiabilidade. + Verifique sua sessão atual para mensageria segura melhorada. + Esta sessão está pronta para mensageria segura. + Sua sessão atual está pronta para mensageria segura. + Criar DM somente em primeira mensagem + Habilitar DMs diferidas + Um Element simplificado com abas opcionais + Habilitar novo layout diff --git a/library/ui-strings/src/main/res/values-pt/strings.xml b/library/ui-strings/src/main/res/values-pt/strings.xml index 4daaef83b0..87b6297b2b 100644 --- a/library/ui-strings/src/main/res/values-pt/strings.xml +++ b/library/ui-strings/src/main/res/values-pt/strings.xml @@ -246,7 +246,7 @@ Note que esta acção irá reiniciar a aplicação e poderá levar algum tempo.< Erro de decifragem Nome do dispositivo - ID do dispositivo + ID do dispositivo Chave do dispositivo Exportar chaves E2E da sala Exportar chaves de sala diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 4852be1f82..c8eee49d96 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -273,7 +273,7 @@ Фильтр названий комнат Приглашения Маловажные - Беседы + Личные сообщения Только Matrix контакты Нет результатов Комнаты @@ -432,7 +432,7 @@ Сбросить основной адрес Ошибка дешифровки Публичное имя - ID сессии + ID сессии Ключ сессии Экспорт E2E ключей комнаты Экспорт ключей комнаты @@ -452,7 +452,7 @@ Чтобы убедиться, что этой сессии можно доверять, обратитесь к ее владельцу, используя другие способы (например, лично или по телефону), и спросите, соответствует ли ключ, который он видит в настройках для этой сессии: Если они не совпадают, безопасность вашего общения может быть поставлена под угрозу. Выбор каталога комнат - Имя сервера + Название сервера Все комнаты на сервере %s Все местные комнаты %s Пользовательский интерфейс @@ -907,16 +907,16 @@ Событие удалено пользователем Событие модерируется администратором комнаты Некорректное событие, не могу отобразить - Создать новую комнату + Создать комнату Нет сети. Пожалуйста, проверьте подключение к Интернету. Изменить - Изменить сеть + Изменить сервер Пожалуйста, подождите… Эту комнату нельзя предварительно просмотреть Комнаты Личные сообщения СОЗДАТЬ - Имя + Название Публичная Каждый сможет присоединиться к этой комнате Произошла ошибка при получении информации о доверии @@ -927,7 +927,7 @@ Вы уже просмотрели эту комнату! Общее Предпочтения - Безопасность и конфиденциальность + Безопасность Правила push-уведомлений app_id: push_key: @@ -956,11 +956,11 @@ Изменения не найдены Отфильтровать беседы… Не можете найти нужное\? - Создать новую комнату - Отправить новое личное сообщение - Просмотр каталога комнат - Имя или ID (#example:matrix.org) - Включить жест смахивания для ответа в ленте сообщений + Создать комнату + Отправить личное сообщение + Каталог комнат + Название или ID (#example:matrix.org) + Жест смахивания для ответа в ленте сообщений Ссылка скопирована в буфер обмена Создаем комнату… История изменений @@ -1039,7 +1039,7 @@ Использовать камеру Использовать микрофон Получать доступ к медиа, защищённым DRM - Создать новую комнату + Создать комнату Файл Камера Галерея @@ -1390,7 +1390,7 @@ Вы приняли Подтверждение отправлено Запрос на подтверждение - Подтвердите эту сессию + Заверьте эту сессию Сканируйте код с помощью устройства другого пользователя, чтобы безопасно проверить друг друга Сканировать их код Невозможно сканировать @@ -1450,7 +1450,7 @@ %d сессии активны %d сессий активно - Подтвердите это устройство + Заверьте эту сессию Используйте существующую сессию для подтверждения этой, предоставив ей доступ к зашифрованным сообщениям. Инструменты для разработчиков Данные учётной записи @@ -1473,13 +1473,13 @@ Безопасное резервное копирование Эта сессия является надежной для безопасного обмена сообщениями, поскольку вы подтвердили ее: Подтвердите эту сессию, чтобы пометить её доверенной и предоставить ей доступ к зашифрованным сообщениям. Если вы не входили в эту сессию, ваша учетная запись может быть скомпрометирована: - Проверить - Проверено + Заверить + Заверено Предупреждение Не удалось получить список сессий Сессии - Доверенные - Недоверенные + Заверенная + Незаверенная Эта сессия является доверенной для безопасного обмена сообщениями, так как %1$s (%2$s) проверил(а) его: %1$s (%2$s) вошел(ла), используя новую сессию: Пока этот пользователь не доверяет этой сессии, сообщения, отправленные в обе стороны, помечаются предупреждениями. Кроме того, вы можете подтвердить сессию вручную. @@ -2037,7 +2037,7 @@ Вы здесь единственный человек. Если вы уйдёте, никто не сможет присоединиться в будущем, включая вас. Покинуть Добавить комнаты - Исследуйте комнаты + Обзор комнат %d человек, которого вы знаете, уже присоединился %d людей, которых вы знаете, уже присоединились @@ -2116,7 +2116,7 @@ Сканируйте код с помощью другого устройства или переключитесь и сканируйте с помощью этого устройства Адрес пространства Файл слишком большой для загрузки. - Поиск по имени + Поиск по названию Сжатие видео %d%% Сжатие изображения… Оставить отзыв @@ -2374,11 +2374,11 @@ Опрос Создать опрос Перезапустите приложение, чтобы изменения вступили в силу. - Включить математику LaTeX + Математика LaTeX Ваша система будет автоматически отправлять журналы при возникновении ошибки невозможности расшифровки Автоматически сообщать об ошибках расшифровки. Шифрование неправильно настроено - Изменить цвет отображаемого имени + Изменить цвет имени Восстановить шифрование Обратитесь к администратору, чтобы восстановить шифрование до рабочего состояния. Шифрование настроено неправильно. @@ -2435,7 +2435,7 @@ Не удалось загрузить карту Карта Примечание: приложение будет перезапущено - Включить обсуждения сообщений + Обсуждения сообщений Подключиться к серверу Хотите присоединиться к существующему серверу\? Пропустить вопрос @@ -2507,7 +2507,7 @@ Идёт отправка местоположения Осталось %1$s Обновлено %1$s назад - Включить функцию \"Поделиться трансляцией местоположения\" + Функция \"Поделиться трансляцией местоположения\" ${app_name} Трансляция местоположения Транслировать до %1$s Трансляция завершена @@ -2660,13 +2660,13 @@ Не удалось загрузить карту \nВозможно, этот домашний сервер не настроен для отображения карт. Все беседы - Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете. - Другие сессии + Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете. + Другие сессии Сессии Создать беседу или комнату Показать все сессии (V2, в разработке) - Люди - Настройки макета + ЛС + Настройки вида Фильтры Недавние Избранные @@ -2676,6 +2676,70 @@ Активности Сортировать по Обзор комнат - Начать беседу + Отправить ЛС Создать комнату + Посмотреть все (%1$d) + Повысьте безопасность учётной записи, следуя этим рекомендациям. + Заверенная · Последняя активность %1$s + Незаверенная сессия + Заверенная сессия + Неизвестный тип устройства + Компьютер + Мобильный + Незаверенная · Последняя активность %1$s + Рекомендации по безопасности + Незаверенные сессии + Неактивные сессии + Добро пожаловать в ${app_name}, +\n%s. + Оставить отзыв + Название сессии + Неактивные + IP-адрес + Последняя активность + Сведения о сессии + Для лучшей безопасности выйдите из всех сессий, которые более не признаёте или не используете. + Заверенные + Все сессии + Последняя активность %1$s + Устройство + Сессия + Текущая сессия + Заверить сессию + Подробности + Эта сессия готова к безопасному обмену сообщениями. + Текущая сессия готова к безопасному обмену сообщениями. + Веб-браузер + Пространства — это новый способ организации комнат и людей. Создайте пространство, чтобы начать. + Новый вид + Нечего отображать. + Здесь будут отображаться непрочитанные сообщения, когда таковые будут. + Присущий системе + Смена пространства + Упрощённый Element с дополнительными вкладками + Добро пожаловать в новый вид! + %s +\nвыглядит слегка пустовато. + Попробовать + Сведения о приложении, устройстве и активности. + Подтвердите текущую сессию для более безопасного обмена сообщениями. + Пока нет пространств. + Подтвердите свои сессии для более безопасного обмена сообщениями или выйдите из тех, которые более не признаёте или не используете. + Подтвердите или выйдите из незаверенных сессий. + Подтвердите или выйдите из этой сессии для лучшей безопасности и надёжности. + Ничего нового. + Заверенных сессий не обнаружено. + Незаверенных сессий не обнаружено. + Неактивных сессий не обнаружено. + Очистить фильтр + Не готовы к безопасному обмену сообщениями + Готовы к безопасному обмену сообщениями + + Неактивны %1$d день или дольше + Неактивны %1$d дня или дольше + Неактивны %1$d дней или дольше + Неактивны %1$d дней или дольше + + Незаверенные + Фильтр \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 2cc2d0280e..f37af1a654 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -388,7 +388,7 @@ Vzhľad Chyba dešifrovania Verejné meno - ID relácie + ID relácie Kľúč relácie Exportovať šifrovacie kľúče miestnosti Exportovať kľúče miestnosti @@ -2651,8 +2651,8 @@ Otvoriť nastavenia Všetky konverzácie Zobraziť všetky relácie (V2, WIP) - V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate. - Iné relácie + V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate. + Iné relácie Relácie Otvoriť zoznam priestorov Vytvoriť novú konverzáciu alebo miestnosť @@ -2672,11 +2672,8 @@ Neoverené - Posledná aktivita %1$s Overené - Posledná aktivita %1$s Zobraziť všetky (%1$d) - Aktuálna relácia Zobraziť podrobnosti Overiť reláciu - Overte svoju aktuálnu reláciu pre vylepšené bezpečné zasielanie správ. - Vaša aktuálna relácia je pripravená na bezpečné zasielanie správ. Neoverená relácia Overená relácia Neznámy typ zariadenia @@ -2686,4 +2683,85 @@ Je nám ľúto, táto miestnosť nebola nájdená. \nProsím, skúste to neskôr.%s Pozvánky - + Vyskúšajte si to + Ťuknutím na položku vpravo hore zobrazíte možnosť spätnej väzby. + Poskytnite spätnú väzbu + Získajte prístup k svojim priestorom (vľavo dole) rýchlejšie a jednoduchšie ako kedykoľvek predtým. + Prístup k priestorom + Pre zjednodušenie vašej aplikácie ${app_name}, sú teraz karty voliteľné. Spravujte ich pomocou ponuky vpravo hore. + Vitajte v novom zobrazení! + Tu sa zobrazia neprečítané správy, ak nejaké máte. + Nič, o čom by bolo potrebné podať správu. + Kompletná zabezpečená aplikácia na komunikáciu pre tímy, priateľov a organizácie. Začnite konverzáciu alebo sa pridajte k existujúcej miestnosti. + Vitajte v aplikácii ${názov_aplikácie}, +\n%s. + Priestory sú novým spôsobom zoskupovania miestností a ľudí. Pomocou tlačidla vpravo dole môžete pridať existujúcu miestnosť alebo vytvoriť novú. + %s +\nvyzerá trochu prázdne. + + Zvážte odhlásenie zo starých relácií (%1$d deň alebo viac), ktoré už nepoužívate. + Zvážte odhlásenie zo starých relácií (%1$d dni alebo viac), ktoré už nepoužívate. + Zvážte odhlásenie zo starých relácií (%1$d dní alebo viac), ktoré už nepoužívate. + + Neaktívne relácie + Overte alebo sa odhláste z neoverených relácií. + Neoverené relácie + Zlepšite zabezpečenie svojho účtu dodržiavaním týchto odporúčaní. + Bezpečnostné odporúčania + + Neaktívny už %1$d+ deň (%2$s) + Neaktívny už %1$d+ dni (%2$s) + Neaktívny už %1$d+ dní (%2$s) + + Tu sa budú nachádzať vaše nové žiadosti a pozvánky. + Nič nové. + Priestory sú novým spôsobom zoskupovania miestností a ľudí. Vytvorte si priestor a začnite. + Zatiaľ žiadne priestory. + Zbaliť %s podpriestory + Rozbaliť %s podpriestory + Zmeniť priestor + IP adresa + Posledná aktivita + Názov relácie + Informácie o aplikácii, zariadení a činnosti. + Podrobnosti o relácii + Zrušiť filter + Nenašli sa žiadne neaktívne relácie. + Nenašli sa žiadne neoverené relácie. + Nenašli sa žiadne overené relácie. + + Zvážte odhlásenie zo starých relácií (%1$d deň alebo viac), ktoré už nepoužívate. + Zvážte odhlásenie zo starých relácií (%1$d dni alebo viac), ktoré už nepoužívate. + Zvážte odhlásenie zo starých relácií (%1$d dní alebo viac), ktoré už nepoužívate. + + Neaktívne + Overte si relácie pre vylepšené bezpečné zasielanie správ alebo sa odhláste z tých, ktoré už nepoznáte alebo nepoužívate. + Neoverené + V záujme čo najlepšieho zabezpečenia sa odhláste z každej relácie, ktorú už nepoznáte alebo nepoužívate. + Overené + Filter + + Neaktívny už %1$d deň alebo dlhšie + Neaktívny už %1$d dni alebo dlhšie + Neaktívny už %1$d dní alebo dlhšie + + Neaktívne + Nie je pripravené na bezpečné zasielanie správ + Neoverené + Pripravené na bezpečné zasielanie správ + Overené + Všetky relácie + Filter + Posledná aktivita %1$s + Zariadenie + Relácia + Aktuálna relácia + V záujme čo najvyššej bezpečnosti a spoľahlivosti túto reláciu overte alebo sa z nej odhláste. + Overte svoju aktuálnu reláciu pre vylepšené bezpečné zasielanie správ. + Táto relácia je pripravená na bezpečné zasielanie správ. + Vaša aktuálna relácia je pripravená na bezpečné zasielanie správ. + Vytvoriť priamu správu len pri prvej správe + Povoliť odložené priame správy + Zjednodušený Element s voliteľnými kartami + Zapnúť nové usporiadanie + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 8fdf4ee310..a6af0a4921 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -431,7 +431,7 @@ Temë Gabim shfshehtëzimi Emër publik - ID Sesioni + ID Sesioni Kyç sesioni Eksporto kyçe dhome E2E Eksporto kyçe dhome diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index 025713272c..30b63c213c 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -918,7 +918,7 @@ Sätt upp på den här enheten Generera en ny säkerhetskopia eller sätt en ny lösenfras för din existerande säkerhetskopia. Detta är experimentella funktioner som kan gå sönder på oväntade sätt. Använd varsamt. - Sessions-ID + Sessions-ID Sessionsnyckel Exportera krypteringsnycklar Exportera rumsnycklar diff --git a/library/ui-strings/src/main/res/values-te/strings.xml b/library/ui-strings/src/main/res/values-te/strings.xml index 5ed2462ce8..0154d54c2e 100644 --- a/library/ui-strings/src/main/res/values-te/strings.xml +++ b/library/ui-strings/src/main/res/values-te/strings.xml @@ -260,7 +260,7 @@ ప్రధాన చిరునామాగా సెట్ చేయండి పరికరం పేరు - పరికరం ID + పరికరం ID పరికరం కీ E2E గది కీలను ఎగుమతి చేయండి diff --git a/library/ui-strings/src/main/res/values-tr/strings.xml b/library/ui-strings/src/main/res/values-tr/strings.xml index c097bfce6a..1f0e5be153 100644 --- a/library/ui-strings/src/main/res/values-tr/strings.xml +++ b/library/ui-strings/src/main/res/values-tr/strings.xml @@ -376,7 +376,7 @@ Tema Çözme hatası Görünür Ad - Oturum kimliği + Oturum kimliği Oturum anahtarı E2E Oda anahtarlarını dışa aktar Oda anahtarlarını dışa aktar diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 1c809fff3e..c4f1658f6b 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -354,7 +354,7 @@ Зробити не основною адресою Помилка розшифрування Загальнодоступна назва - ID сеансу + ID сеансу Ключ сеансу Експортувати E2E ключі кімнати Експортувати ключі кімнати @@ -818,7 +818,7 @@ URL-адреса аватара Ваше показуване ім\'я Скасувати доступ для мене - Відкрити в переглядачі + Відкрити у браузері Перезавантажити віджет Не вдалося завантажити віджет. \n%s @@ -1196,7 +1196,7 @@ Використати файл Скористатись парольною фразою відновлення або ключем Скористатись відновлювальними парольною фразою або ключем - Використовуйте найостаннішій ${app_name} на ваших інших пристроях, ${app_name} Web, ${app_name} для комп\'ютерів, ${app_name} iOS, ${app_name} для Android, або будь-який інший, здатний до перехресного підписування, Matrix-клієнт + Використовуйте найостаннішій ${app_name} на ваших інших пристроях, ${app_name} браузері, ${app_name} комп\'ютерах, ${app_name} iOS, ${app_name} Android, або будь-який інший, здатний до перехресного підписування, Matrix-клієнт Використовуйте найостаннішій ${app_name} на ваших інших пристроях: Якщо ви не можете доступитись до чинного сеансу Використайте чинний сеанс, щоб звірити цей сеанс, таким чином надавши йому доступ до зашифрованих повідомлень. @@ -2021,7 +2021,7 @@ Не вдалося отримати доступ до безпечного сховища ${app_name} iOS \n${app_name} Android - ${app_name} для переглядача + ${app_name} для браузера \n${app_name} для ПК Не вдалося зберегти медіафайл Це не дійсний ключ відновлення @@ -2701,8 +2701,8 @@ Відкрити налаштування Усі бесіди Показати всі сеанси (V2, WIP) - Для найкращої безпеки перевірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте. - Інші сеанси + Звірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте для кращої безпеки. + Інші сеанси Сеанси Відкрити список кімнат Створити нову розмову або кімнату @@ -2722,11 +2722,8 @@ Не звірений · Остання активність %1$s Звірений · Остання активність %1$s Переглянути всі (%1$d) - Поточний сеанс Переглянути подробиці Звірити сеанс - Звірте свій поточний сеанс для безпечнішого обміну повідомленнями. - Ваш поточний сеанс готовий для безпечного обміну повідомленнями. Не звірений сеанс Звірений сеанс Невідомий тип пристрою @@ -2736,4 +2733,89 @@ Перепрошуємо, цю кімнату не знайдено. \nСпробуйте пізніше.%s Запрошення - + Спробувати + Клацніть праворуч вгорі, щоб побачити опцію відгуку. + Надіслати відгук + Отримуйте доступ до своїх просторів (унизу праворуч) швидше та легше, ніж раніше. + Доступ до просторів + Щоб спростити ваш ${app_name}, вкладки тепер необов’язкові. Керуйте ними у верхньому правому меню. + Вітаємо в новому вигляді! + Тут з\'являтимуться ваші непрочитані повідомлення, якщо вони є. + Немає про що звітувати. + Універсальний безпечний застосунок для спілкування з командами, друзями й організаціями. Створіть бесіду або приєднайтеся до наявної кімнати, щоб розпочати. + Вітаємо в ${app_name}, +\n%s. + Простори – це новий спосіб групувати кімнати та людей. Додайте наявну кімнату або створіть нову, використовуючи кнопку внизу праворуч. + %s +\nмає дещо порожній вигляд. + + Зважте потребу вийти зі старих сеансів (%1$d день або більше), який ви більше не використовуєте. + Зважте потребу вийти зі старих сеансів (%1$d дні або більше), які ви більше не використовуєте. + Зважте потребу вийти зі старих сеансів (%1$d днів або більше), які ви більше не використовуєте. + Зважте потребу вийти зі старих сеансів (%1$d днів або більше), які ви більше не використовуєте. + + Неактивні сеанси + Звірити або вийти з не звірених сеансів. + Не звірений сеанс + Удоскональте безпеку свого облікового запису, дотримуючись цих порад. + Поради щодо безпеки + + Без активності %1$d+ день (%2$s) + Без активності %1$d+ дні (%2$s) + Без активності %1$d+ днів (%2$s) + Без активності %1$d+ днів (%2$s) + + Тут з\'являтимуться нові запити та запрошення. + Нічого нового. + Простори – це новий спосіб групувати кімнати та людей. Створіть простір, щоб розпочати. + Ще немає просторів. + Згорнути дочірні елементи %s + Розгорнути дочірні елементи %s + Змінити простір + IP-адреса + Остання активність + Назва сеансу + Відомості про застосунок, пристрій та діяльність. + Подробиці сеансу + Очистити фільтр + Неактивних сеансів не знайдено. + Не знайдено не звірених сеансів. + Знайдені не звірені сеанси. + + Подумайте про те, щоб вийти зі старих сеансів (%1$d день або довше), якими ви більше не користуєтесь. + Подумайте про те, щоб вийти зі старих сеансів (%1$d дні або довше), якими ви більше не користуєтесь. + Подумайте про те, щоб вийти зі старих сеансів (%1$d днів або довше), якими ви більше не користуєтесь. + Подумайте про те, щоб вийти зі старих сеансів (%1$d днів або довше), якими ви більше не користуєтесь. + + Неактивний + Звірте свої сеанси для посилення безпеки обміну повідомленнями або вийдіть з тих, які ви більше не впізнаєте або не використовуєте. + Не звірений + Для кращої безпеки виходьте з будь-якого сеансу, який ви більше не впізнаєте або не використовуєте. + Звірений + Фільтрувати + + Неактивний %1$d день або довше + Неактивний %1$d дні або довше + Неактивний %1$d днів або довше + Неактивний %1$d днів або довше + + Неактивний + Не готовий до безпечного обміну повідомленнями + Не звірений + Звірений + Готовий до безпечного обміну повідомленнями + Усі сеанси + Фільтрувати + Остання активність %1$s + Пристрій + Сеанс + Поточний сеанс + Звірте або вийдіть з цього сеансу для кращої безпеки та надійності. + Звірте свій поточний сеанс для посилення безпеки обміну повідомленнями. + Цей сеанс готовий до безпечного обміну повідомленнями. + Ваш поточний сеанс готовий до безпечного обміну повідомленнями. + Створюйте приватні повідомлення лише за надсилання першого повідомлення + Увімкнути відкладені приватні повідомлення + Спрощений Element з опціональними вкладками + Увімкнути новий вигляд + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index 2803128843..c6dc97f782 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -594,7 +594,7 @@ Hủy tài khoản Xem lại ngay Chìa khóa phiên - Mã phiên + Mã phiên Tên công khai Lỗi giải mã Những chức năng này mang tính thí nghiệm có thể còn nhiều lỗi. Lưu ý khi dùng. diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 4e1c8e61c8..eba96e82c3 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -242,7 +242,7 @@ 你的密码已更新 解密错误 公开名称 - 会话 ID + 会话 ID 会话密钥 导入 已验证 @@ -2500,7 +2500,7 @@ 若启用,即使正在使用应用,你也会对其他用户显示为离线状态。 离线模式 在场 - 动画图片一出现就在时间轴中播放 + 动画图片一出现就在时间线中播放 Threads Beta ${app_name} needs to perform a clear cache to be up to date, 原因如下: \n%s @@ -2551,8 +2551,8 @@ 打开设置 全部聊天 显示全部会话(V2, WIP) - 为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。 - 其他会话 + 为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。 + 其他会话 会话 打开空间列表 创建新对话或房间 @@ -2574,13 +2574,54 @@ 未验证 · 上次活跃 %1$s 已验证 · 上次活跃 %1$s 查看全部(%1$d) - 当前会话 查看详情 验证会话 - 为了获得增强的安全的消息传送,请验证你当前的会话。 - 你的当前会话已准备好安全地收发消息。 未验证的会话 已验证的会话 未知的设备类型 邀请 + 移动设备 + Web + 桌面 + 更改空间 + 尚无空间。 + 没有新的东西。 + 你的新请求和邀请会在这里。 + + %1$d+天不活跃(%2$s) + + 安全建议 + 按照这些建议改善你的账户安全。 + 未验证的会话 + 验证未验证的会话或从之登出。 + 不活跃的会话 + + 请考虑从不再使用的旧会话(%1$d天或更久)登出。 + + 欢迎来到${app_name}, +\n%s。 + 未读消息会在这里显示。 + 提供反馈 + 点击右上角查看反馈选项。 + 试用 + 空间是对房间和人进行分组的新方式。创建一个空间来开始吧。 + 启用新布局 + IP地址 + 验证你的会话以增强消息传输的安全性,或从那些你不认识或不再使用的会话登出。 + 尚未准备好安全收发消息 + 准备好安全收发消息 + 已验证 + 全部会话 + 筛选 + 上次活跃%1$s + 设备 + 会话 + 当前会话 + 验证你的会话以增强消息传输的安全性。 + 访问你的空间(右下角)比以前更快、更容易。 + 此会话已准备好安全地收发消息。 + 你当前的会话已准备好安全地收发消息。 + 仅在首条消息创建私聊消息 + 启用延迟的私聊消息 + 简化的Element,带有可选的标签 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 0f5208bcde..876084d566 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -469,7 +469,7 @@ 主題 解密錯誤 公開名稱 - 工作階段 ID + 工作階段 ID 工作階段金鑰 匯出聊天室的端到端加密金鑰 匯出聊天室的加密金鑰 @@ -2551,8 +2551,8 @@ 開啟設定 所有聊天 顯示所有工作階段 (V2, WIP) - 為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。 - 其他工作階段 + 為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。 + 其他工作階段 工作階段 開啟空間清單 建立新的對話或聊天室 @@ -2572,11 +2572,8 @@ 未驗證 · 最後活動 %1$s 已驗證 · 最後活動 %1$s 檢視全部 (%1$d) - 目前工作階段 檢視詳細資訊 驗證工作階段 - 驗證您目前的工作階段以強化安全通訊。 - 您目前的工作階段已準備好進行安全通訊。 未驗證的工作階段 已驗證的工作階段 未知的裝置類型 @@ -2586,4 +2583,77 @@ 抱歉,找不到此聊天室。 \n請稍後再試。%s 邀請 - + 試試看 + 輕點右上角來檢視回饋選項。 + 給予回饋 + 存取您的空間(右下角)比以往任何時候都更快且更輕鬆。 + 存取空間 + 為了簡化您的 ${app_name},分頁現在是選擇性的。使用右上角的選單管理它們。 + 歡迎使用新的檢視! + 當您有一些未讀的訊息時,這裡會顯示您的未讀訊息。 + 沒有要回報的東西。 + 適用於團隊、朋友與組織的多合一安全聊天應用程式。建立聊天室,或加入一個既有的聊天室。 + 歡迎使用 ${app_name}, +\n%s. + 空間是一種為聊天室與人們分組的新方式。使用右下角的按鈕新增既有的聊天室或建立新的。 + %s +\n看起來有點空。 + + 考慮登出您不再使用的舊工作階段(%1$d天或更久)。 + + 不活躍的工作階段 + 驗證或從未驗證的工作階段登出。 + 未驗證的工作階段 + 按照這些建議提高您的帳號安全性。 + 安全建議 + + 不活躍 %1$d+ 天 (%2$s) + + 這是您的新請求與邀請的所在。 + 沒有新東西。 + 空間是一種對聊天室與人們分組的新方式。建立空間以開始。 + 尚無空間。 + 折疊 %s 個子空間 + 展開 %s 個子空間 + 變更空間 + IP 位置 + 最後活動 + 工作階段名稱 + 應用程式、裝置與活動資訊。 + 工作階段詳細資訊 + 清除過濾條件 + 找不到不活躍的工作階段。 + 找不到未驗證的工作階段。 + 找不到已驗證的工作階段。 + + 閒置%1$d天或更久 + + + 考慮登出您不再使用的舊工作階段(%1$d天或更久)。 + + 不活躍 + 驗證您的工作階段以強化安全通訊或從您無法識別或不再使用的工作階段登出。 + 未驗證 + 為取得最佳安全性,請從任何您無法識別或不再使用的工作階段登出。 + 已驗證 + 過濾 + 不活躍 + 尚未準備好安全通訊 + 未驗證 + 準備好安全通訊 + 已驗證 + 所有工作階段 + 過濾 + 最後活動 %1$s + 裝置 + 工作階段 + 目前的工作階段 + 驗證或從此工作階段登出以取得最佳安全性與可靠性。 + 驗證您目前的工作階段以強化安全通訊。 + 此工作階段已準備好安全通訊。 + 您目前的工作階段已準備好安全通訊。 + 僅在第一則訊息上建立直接訊息 + 啟用延期直接訊息 + 包含選擇性分頁的簡潔 Element + 啟用新佈局 + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 5af3d9e7f6..02e395eb89 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -284,8 +284,8 @@ %1$s turned on end-to-end encryption. You turned on end-to-end encryption. - %1$s turned on end-to-end encryption (unrecognised algorithm %2$s). - You turned on end-to-end encryption (unrecognised algorithm %1$s). + %1$s turned on end-to-end encryption (unrecognized algorithm %2$s). + You turned on end-to-end encryption (unrecognized algorithm %1$s). System Default @@ -406,6 +406,7 @@ Reset Learn more Next + Got it Copied to clipboard @@ -423,7 +424,7 @@ Notifications - Favourites + Favorites People Rooms @@ -442,6 +443,9 @@ Enable new layout A simplified Element with optional tabs + Enable deferred DMs + Create DM only on first message + Invites Low priority @@ -770,7 +774,7 @@ Shows all threads from current room My Threads Shows all threads you’ve participated in - Keep discussions organised with threads + Keep discussions organized with threads Threads help keep your conversations on-topic and easy to track. Tip: Long tap a message and use “%s”. @@ -815,7 +819,7 @@ Show the application info in the system settings. Email addresses - No email has been added to your account + No email address has been added to your account Phone numbers Remove %s? Ensure that you have clicked on the link in the email we have sent to you. @@ -824,7 +828,7 @@ Notification importance by event Email notification - To receive email with notification, please associate an email to your Matrix account + To receive email with notification, please associate an email address to your Matrix account Enable email notifications for %s @@ -1090,7 +1094,7 @@ Show all messages from %s? Email - Manage emails and phone numbers linked to your Matrix account + Manage email addresses and phone numbers linked to your Matrix account Choose a country @@ -1231,6 +1235,9 @@ Import Encrypt to verified devices only Never send encrypted messages to unverified devices from this device. + Never send encrypted messages to unverified sessions in this room. + ⚠ There are unverified devices in this room, they won’t be able to decrypt messages you send. + 🔒 You have enabled encrypt to verified sessions only for all rooms in Security Settings. %1$d/%2$d key imported with success. %1$d/%2$d keys imported with success. @@ -1402,6 +1409,7 @@ Changes your avatar in this current room only On/Off markdown To fix Matrix Apps management + Open the developer tools screen Displays information about a user Markdown has been enabled. @@ -1638,7 +1646,7 @@ All Unreads - Favourites + Favorites People Reactions @@ -1797,20 +1805,20 @@ You are currently using %1$s to discover and be discoverable by existing contacts you know. You are not currently using an identity server. To discover and be discoverable by existing contacts you know, configure one below. Discoverable email addresses - Discovery options will appear once you have added an email. + Discovery options will appear once you have added an email address. Discovery options will appear once you have added a phone number. Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone. Discoverable phone numbers - We sent you a confirm email to %s, check your email and click on the confirmation link - We sent you a confirm email to %s, please first check your email and click on the confirmation link + We sent an email to %s, check your email and click on the confirmation link + We sent an email to %s, please first check your email and click on the confirmation link Send emails and phone numbers - You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts. + You have given your consent to send email addresses and phone numbers to this identity server to discover other users from your contacts. Your contacts are private. To discover users from your contacts, we need your permission to send contact info to your identity server. Revoke my consent Give consent - Send emails and phone numbers to %s - To discover existing contacts, you need to send contact info (emails and phone numbers) to your identity server. We hash your data before sending for privacy. + Send email addresses and phone numbers to %s + To discover existing contacts, you need to send contact info (email addresses and phone numbers) to your identity server. We hash your data before sending for privacy. Do you agree to send this info? Enter an identity server URL @@ -1840,7 +1848,7 @@ Close the create room menu… Create a new direct conversation Create a new conversation or room - Create a new room + Create a new room Open spaces list Close keys backup banner Jump to bottom @@ -1868,6 +1876,7 @@ "Sticker" Poll Location + Voice Broadcast Rotate and crop Couldn\'t handle share data @@ -2034,7 +2043,7 @@ It\'s your conversation. Own it. Chat with people directly or in groups Keep conversations private with encryption - Extend & customise your experience + Extend & customize your experience Get started Create account I already have an account @@ -2077,7 +2086,7 @@ Sorry, this server isn’t accepting new accounts. The application is not able to create an account on this homeserver. - This email is not associated to any account. + This email address is not associated to any account. Reset password on %1$s @@ -2090,7 +2099,7 @@ Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Export your room keys from another session before resetting your password. Continue - This email is not linked to any account + This email address is not linked to any account Check your inbox @@ -2107,7 +2116,7 @@ Your password is not yet changed.\n\nStop the password change process? Set email address - Set an email to recover your account. Later, you can optionally allow people you know to discover you by your email. + Set an email address to recover your account. Later, you can optionally allow people you know to discover you by your this address. Email Email (optional) Next @@ -2217,6 +2226,7 @@ Prepends ¯\\_(ツ)_/¯ to a plain-text message Prepends ( ͡° ͜ʖ ͡°) to a plain-text message + Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message "Enable encryption" "Once enabled, encryption cannot be disabled." @@ -2257,8 +2267,8 @@ Shared their live location Waiting… - %s cancelled - You cancelled + %s canceled + You canceled %s accepted You accepted Verification Sent @@ -2362,9 +2372,6 @@ Manage Sessions Sign out of this session Sessions - Other sessions - For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. - Server name Server version Server file upload limit @@ -2403,7 +2410,7 @@ This session is trusted for secure messaging because %1$s (%2$s) verified it: %1$s (%2$s) signed in using a new session: - Until this user trusts this session, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it. + Until this user trusts this session, messages sent to and from it are labeled with warnings. Alternatively, you can manually verify it. Initialize CrossSigning @@ -2472,9 +2479,9 @@ One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password & recovery key in Settings immediately. - Verification has been cancelled. You can start verification again. + Verification has been canceled. You can start verification again. This QR code looks malformed. Please try to verify with another method. - Verification Cancelled + Verification Canceled Recovery Passphrase Message Key @@ -2575,6 +2582,9 @@ Prevent screenshots of the application Enabling this setting adds the FLAG_SECURE to all Activities. Restart the application for the change to take effect. + Incognito keyboard + "Request that the keyboard should not update any personalized data such as typing history and dictionary based on what you've typed in conversations. Notice that some keyboards may not respect this setting." + Could not save media file Set a new account password… @@ -2613,6 +2623,7 @@ Unencrypted Encrypted by an unverified device + The authenticity of this encrypted message can\'t be guaranteed on this device. Review where you’re logged in Verify all your sessions to ensure your account & messages are safe @@ -2670,7 +2681,7 @@ Please first configure an identity server. Please first accepts the terms of the identity server in the settings. - For your privacy, ${app_name} only supports sending hashed user emails and phone number. + For your privacy, ${app_name} only supports sending hashed user email addresses and phone numbers. The association has failed. There is no current association with this identifier. The user consent has not been provided. @@ -2909,7 +2920,7 @@ Who are you working with? Make sure the right people have access to %s. Just me - A private space to organise your rooms + A private space to organize your rooms Me and teammates A private space for you & your teammates Public @@ -3067,7 +3078,7 @@ This invite to this space was sent to %s which is not associated with your account - Link this email with your account + Link this email address with your account %s in Settings to receive invites directly in ${app_name}. @@ -3174,6 +3185,7 @@ Open contacts Create poll Share location + Start a voice broadcast Show less @@ -3224,29 +3236,26 @@ Show All Sessions (V2, WIP) + Other sessions + For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Mobile Web Desktop Unknown device type Verified session Unverified session - - Your current session is ready for secure messaging. - - Verify your current session for enhanced secure messaging. Your current session is ready for secure messaging. This session is ready for secure messaging. Verify your current session for enhanced secure messaging. Verify or sign out from this session for best security and reliability. Verify Session View Details - - Current Session View All (%1$d) Verified · Last activity %1$s Unverified · Last activity %1$s + Unverified · Your current session Inactive for %1$d+ day (%2$s) @@ -3263,8 +3272,53 @@ Current Session Session + Device Last activity %1$s + Filter + All sessions + Verified + Ready for secure messaging + Unverified + Not ready for secure messaging + Inactive + + Inactive for %1$d day or longer + Inactive for %1$d days or longer + + Filter + Verified + For best security, sign out from any session that you don’t recognize or use anymore. + Unverified + Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore. + Inactive + + Consider signing out from old sessions (%1$d day or more) you don’t use anymore. + Consider signing out from old sessions (%1$d days or more) you don’t use anymore. + + No verified sessions found. + No unverified sessions found. + No inactive sessions found. + Clear Filter + Sign out of this session + Session details + Application, device, and activity information. + Session name + Session ID + Last activity + IP address + Rename session + Session name + Custom session names can help you recognize your devices more easily. + Please be aware that session names are also visible to people you communicate with. + Inactive sessions + Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious. + Unverified sessions + Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. + Verified sessions + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Renaming sessions + Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. %s\nis looking a little empty. diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 2cc801067d..59ef404b71 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -68,5 +68,5 @@ dependencies { // Pref theme implementation libs.androidx.preferenceKtx // dialpad dimen - implementation 'im.dlg:android-dialer:1.2.5' + implementation project(":library:external:dialpad") } diff --git a/library/ui-styles/src/debug/res/menu/menu_debug.xml b/library/ui-styles/src/debug/res/menu/menu_debug.xml index c58a29db8f..ac98ce8e2c 100644 --- a/library/ui-styles/src/debug/res/menu/menu_debug.xml +++ b/library/ui-styles/src/debug/res/menu/menu_debug.xml @@ -14,6 +14,7 @@ android:id="@+id/menuDebug2" android:icon="@drawable/ic_debug_icon" android:title="Send" - app:showAsAction="always" /> + app:showAsAction="always" + tools:ignore="AlwaysShowAction" /> - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 45990a0384..61ee5cb44a 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -144,7 +144,9 @@ #0DBD8B + #0F0DBD8B #17191C + #91A1C0 #FF4B55 #0FFF4B55 diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 758dd6e978..0fb03f0ea3 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -50,9 +50,9 @@ 28dp - 62dp - 300dp - 12dp + 6dp + 350sp + 8dp 0.05 diff --git a/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml deleted file mode 100644 index 97e0290815..0000000000 --- a/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml new file mode 100644 index 0000000000..6a46132b13 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/stylable_session_overview_entry_view.xml b/library/ui-styles/src/main/res/values/stylable_session_overview_entry_view.xml new file mode 100644 index 0000000000..d3884f247d --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_session_overview_entry_view.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml b/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml new file mode 100644 index 0000000000..6236b31f46 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml new file mode 100644 index 0000000000..098ec263fc --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/styles_buttons.xml b/library/ui-styles/src/main/res/values/styles_buttons.xml index 5864e3a760..21b7054d5e 100644 --- a/library/ui-styles/src/main/res/values/styles_buttons.xml +++ b/library/ui-styles/src/main/res/values/styles_buttons.xml @@ -42,6 +42,10 @@ 24sp + + + diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 8be8e83569..a6b4cc98a6 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -46,6 +47,13 @@ class FlowRoom(private val room: Room) { } } + fun liveLocalRoomSummary(): Flow> { + return room.getLocalRoomSummaryLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.localRoomSummary().toOptional() + } + } + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow> { return room.membershipService().getRoomMembersLive(queryParams).asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b9a0820172..7d7bfa166f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -60,7 +60,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.36\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.2\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -222,6 +222,8 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.androidx.coreTesting androidTestImplementation libs.jetbrains.coroutinesAndroid + androidTestImplementation libs.jetbrains.coroutinesTest + // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index 260e8dbe05..403f697778 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) // Change password - commonTestHelper.runBlockingTest { - session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) - } + session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) // Try to login with the previous password, it will fail val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) 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 0b21f85742..bb5618b816 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 @@ -40,26 +40,24 @@ import kotlin.coroutines.resume class DeactivateAccountTest : InstrumentedTest { @Test - fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper -> + fun deactivateAccountTest() = runSessionTest(context(), autoSignoutOnClose = false /* session will be deactivated */) { commonTestHelper -> val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Deactivate the account - commonTestHelper.runBlockingTest { - session.accountService().deactivateAccount( - eraseAllData = false, - userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = session.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } + session.accountService().deactivateAccount( + eraseAllData = false, + userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) } - ) - } + } + ) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) @@ -74,23 +72,19 @@ class DeactivateAccountTest : InstrumentedTest { // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) val hs = commonTestHelper.createHomeServerConfig() - commonTestHelper.runBlockingTest { - commonTestHelper.matrix.authenticationService.getLoginFlow(hs) - } + commonTestHelper.matrix.authenticationService.getLoginFlow(hs) var accountCreationError: Throwable? = null - commonTestHelper.runBlockingTest { - try { - commonTestHelper.matrix.authenticationService - .getRegistrationWizard() - .createAccount( - session.myUserId.substringAfter("@").substringBefore(":"), - TestConstants.PASSWORD, - null - ) - } catch (failure: Throwable) { - accountCreationError = failure - } + try { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount( + session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null + ) + } catch (failure: Throwable) { + accountCreationError = failure } // Test the error 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 f30d2dab81..3b7945a1f6 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 @@ -19,23 +19,22 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri import android.util.Log -import androidx.lifecycle.Observer import androidx.test.internal.runner.junit4.statement.UiThreadStatement -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.SyncConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.crypto.MXCryptoConfig @@ -51,46 +50,56 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.session.sync.SyncState import timber.log.Timber import java.util.UUID -import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * This class exposes methods to be used in common cases * Registration, login, Sync, Sending messages... */ -class CommonTestHelper internal constructor(context: Context) { +class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) { companion object { - internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { - val testHelper = CommonTestHelper(context) - return try { - block(testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context, cryptoConfig) + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } - internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) { - val testHelper = CommonTestHelper(context) + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context, cryptoConfig) val cryptoTestHelper = CryptoTestHelper(testHelper) - return try { - block(cryptoTestHelper, testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(cryptoTestHelper, testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } } internal val matrix: TestMatrix - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var accountNumber = 0 private val trackedSessions = mutableListOf() @@ -105,6 +114,7 @@ class CommonTestHelper internal constructor(context: Context) { MatrixConfiguration( applicationFlavor = "TestFlavor", roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), + syncConfig = SyncConfig(longPollTimeout = 5_000L), // Tchap: Do not limit here key requests to my devices to unblock crypto tests cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false), ) @@ -113,19 +123,17 @@ class CommonTestHelper internal constructor(context: Context) { matrix = _matrix!! } - fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) } - fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + suspend fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { return logIntoAccount(userId, TestConstants.PASSWORD, testParams) } - fun cleanUpOpenedSessions() { + suspend fun cleanUpOpenedSessions() { trackedSessions.forEach { - runBlockingTest { - it.signOutService().signOut(true) - } + it.signOutService().signOut(true) } trackedSessions.clear() } @@ -139,27 +147,10 @@ class CommonTestHelper internal constructor(context: Context) { .build() } - /** - * This methods init the event stream and check for initial sync - * - * @param session the session to sync - */ - fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { - val lock = CountDownLatch(1) - coroutineScope.launch { - session.syncService().startSync(true) - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - lock.countDown() - syncLiveData.removeObserver(this) - } - } - } - syncLiveData.observeForever(syncObserver) - } - await(lock, timeout) + suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { + session.syncService().startSync(true) + val syncLiveData = session.syncService().getSyncStateLive() + syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() } } /** @@ -167,22 +158,11 @@ class CommonTestHelper internal constructor(context: Context) { * * @param session the session to sync */ - fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { - waitWithLatch(timeout) { latch -> - session.clearCache() - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - Timber.v("Clear cache and synced") - syncLiveData.removeObserver(this) - latch.countDown() - } - } - } - syncLiveData.observeForever(syncObserver) - session.syncService().startSync(true) - } + suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { + session.clearCache() + syncSession(session, timeout) + session.syncService().getSyncStateLive().first(timeout) { session.syncService().hasAlreadySynced() } + Timber.v("Clear cache and synced") } /** @@ -192,7 +172,7 @@ class CommonTestHelper internal constructor(context: Context) { * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ - fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { + suspend fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) timeline.start() val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) @@ -205,66 +185,72 @@ class CommonTestHelper internal constructor(context: Context) { /** * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List { + private suspend fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List { val sentEvents = ArrayList(count) (1 until count + 1) .map { "$message #$it" } .chunked(10) .forEach { batchedMessages -> - batchedMessages.forEach { formattedMessage -> - if (rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = rootThreadEventId, - replyInThreadText = formattedMessage - ) - } else { - room.sendService().sendTextMessage(formattedMessage) - } - } - waitWithLatch(timeout) { latch -> - val timelineListener = object : Timeline.Listener { - - override fun onTimelineUpdated(snapshot: List) { - val allSentMessages = snapshot - .filter { it.root.sendState == SendState.SYNCED } - .filter { it.root.getClearType() == EventType.MESSAGE } - .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } - - val hasSyncedAllBatchedMessages = allSentMessages - .map { - it.root.getClearContent().toModel()?.body + waitFor( + continueWhen = { + wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val timelineListener = object : Timeline.Listener { + + override fun onTimelineUpdated(snapshot: List) { + val allSentMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + + val hasSyncedAllBatchedMessages = allSentMessages + .map { + it.root.getClearContent().toModel()?.body + } + .containsAll(batchedMessages) + + if (allSentMessages.size == count) { + sentEvents.addAll(allSentMessages) + } + if (hasSyncedAllBatchedMessages) { + timeline.removeListener(this) + continuation.resume(Unit) + } + } } - .containsAll(batchedMessages) - - if (allSentMessages.size == count) { - sentEvents.addAll(allSentMessages) + timeline.addListener(timelineListener) + } } - if (hasSyncedAllBatchedMessages) { - timeline.removeListener(this) - latch.countDown() + }, + action = { + batchedMessages.forEach { formattedMessage -> + if (rootThreadEventId != null) { + room.relationService().replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = formattedMessage + ) + } else { + room.sendService().sendTextMessage(formattedMessage) + } } } - } - timeline.addListener(timelineListener) - } + ) } return sentEvents } - fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { - waitWithLatch { latch -> - retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(roomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("# TEST", "${otherSession.myUserId} can see the invite") - } + suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("# TEST", "${otherSession.myUserId} can see the invite") } } } // not sure why it's taking so long :/ - runBlockingTest(90_000) { + wrapWithTimeout(90_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") try { otherSession.roomService().joinRoom(roomID) @@ -274,11 +260,9 @@ class CommonTestHelper internal constructor(context: Context) { } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - waitWithLatch { - retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(roomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } @@ -288,7 +272,7 @@ class CommonTestHelper internal constructor(context: Context) { * @param message the message to send * @param numberOfMessages the number of time the message will be sent */ - fun replyInThreadMessage( + suspend fun replyInThreadMessage( room: Room, message: String, numberOfMessages: Int, @@ -306,15 +290,7 @@ class CommonTestHelper internal constructor(context: Context) { // PRIVATE METHODS ***************************************************************************** - /** - * Creates a unique account - * - * @param userNamePrefix the user name prefix - * @param password the password - * @param testParams test params about the session - * @return the session associated with the newly created account - */ - private fun createAccount( + private suspend fun createAccount( userNamePrefix: String, password: String, testParams: SessionTestParams @@ -332,15 +308,7 @@ class CommonTestHelper internal constructor(context: Context) { } } - /** - * Logs into an existing account - * - * @param userId the userId to log in - * @param password the password to log in - * @param testParams test params about the session - * @return the session associated with the existing account - */ - fun logIntoAccount( + suspend fun logIntoAccount( userId: String, password: String, testParams: SessionTestParams @@ -352,32 +320,25 @@ class CommonTestHelper internal constructor(context: Context) { } } - /** - * Create an account and a dedicated session - * - * @param userName the account username - * @param password the password - * @param sessionTestParams parameters for the test - */ - private fun createAccountAndSync( + private suspend fun createAccountAndSync( userName: String, password: String, sessionTestParams: SessionTestParams ): Session { val hs = createHomeServerConfig() - runBlockingTest { + wrapWithTimeout(TestConstants.timeOutMillis) { matrix.authenticationService.getLoginFlow(hs) } - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(60_000L) { matrix.authenticationService .getRegistrationWizard() .createAccount(userName, password, null) } // Perform dummy step - val registrationResult = runBlockingTest(timeout = 60_000) { + val registrationResult = wrapWithTimeout(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .dummy() @@ -392,29 +353,14 @@ class CommonTestHelper internal constructor(context: Context) { return session } - /** - * Start an account login - * - * @param userName the account username - * @param password the password - * @param sessionTestParams session test params - */ - private fun logAccountAndSync( - userName: String, - password: String, - sessionTestParams: SessionTestParams - ): Session { + private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): Session { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) - val session = runBlockingTest { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } + val session = matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") session.open() if (sessionTestParams.withInitialSync) { syncSession(session) @@ -429,25 +375,21 @@ class CommonTestHelper internal constructor(context: Context) { * @param userName the account username * @param password the password */ - fun logAccountWithError( + suspend fun logAccountWithError( userName: String, password: String ): Throwable { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) var requestFailure: Throwable? = null - runBlockingTest { - try { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } catch (failure: Throwable) { - requestFailure = failure - } + try { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") + } catch (failure: Throwable) { + requestFailure = failure } assertNotNull(requestFailure) @@ -483,65 +425,48 @@ class CommonTestHelper internal constructor(context: Context) { ) } - suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { - while (true) { - try { - delay(1000) - } catch (ex: CancellationException) { - // the job was canceled, just stop - return + suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) { + wrapWithTimeout(timeout) { + while (!predicate()) { + runBlocking { delay(500) } } - if (condition()) { - latch.countDown() - return - } - } - } - - fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { - val latch = CountDownLatch(1) - val job = coroutineScope.launch(dispatcher) { - block(latch) } - await(latch, timeout, job) } - fun runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { - return runBlocking { - withTimeout(timeout) { - block() + suspend fun waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback { + override fun onSuccess(data: T) { + continuation.resume(data) + } + } + block(callback) } } } - // Transform a method with a MatrixCallback to a synchronous method - inline fun doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { - val lock = CountDownLatch(1) - var result: T? = null - - val callback = object : TestMatrixCallback(lock) { - override fun onSuccess(data: T) { - result = data - super.onSuccess(data) + suspend fun waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): Throwable { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + continuation.resume(failure) + } + } + block(callback) } } - - block.invoke(callback) - - await(lock, timeout) - - assertNotNull(result) - return result!! } /** * Clear all provided sessions */ - fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } + suspend fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } - fun signOutAndClose(session: Session) { + suspend fun signOutAndClose(session: Session) { trackedSessions.remove(session) - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(timeout = 60_000L) { session.signOutService().signOut(true) } // no need signout will close diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 41d0d3a7e8..8cd5bee569 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -32,7 +32,7 @@ data class CryptoTestData( val thirdSession: Session? get() = sessions.getOrNull(2) - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { sessions.forEach { testHelper.signOutAndClose(it) } 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 f36bfb6210..74292daf15 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 @@ -16,19 +16,15 @@ 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 import org.junit.Assert.assertTrue 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.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 @@ -46,22 +42,16 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -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.RoomHistoryVisibility -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.KeyRef -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 @@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = roomHistoryVisibility - name = "MyRoom" - }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = roomHistoryVisibility + name = "MyRoom" + }) if (encryptedRoom) { - testHelper.waitWithLatch { latch -> - val room = aliceSession.getRoom(roomId)!! - room.roomCryptoService().enableEncryption() - val roomSummaryLive = room.getRoomSummaryLive() - val roomSummaryObserver = object : Observer> { - override fun onChanged(roomSummary: Optional) { - if (roomSummary.getOrNull()?.isEncrypted.orFalse()) { - roomSummaryLive.removeObserver(this) - latch.countDown() - } - } - } - roomSummaryLive.observeForever(roomSummaryObserver) - } + val room = aliceSession.getRoom(roomId)!! + waitFor( + continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } }, + action = { room.roomCryptoService().enableEncryption() } + ) } return CryptoTestData(roomId, listOf(aliceSession)) } @@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (t?.isNotEmpty() == true) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - aliceRoom.membershipService().invite(bobSession.myUserId) - } + waitFor( + continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } }, + action = { aliceRoom.membershipService().invite(bobSession.myUserId) } + ) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val roomJoinedObserver = object : Observer> { - override fun onChanged(t: List?) { - if (bobSession.getRoom(aliceRoomId) - ?.membershipService() - ?.getRoomMember(bobSession.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() + waitFor( + continueWhen = { + bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { + bobSession.getRoom(aliceRoomId) + ?.membershipService() + ?.getRoomMember(bobSession.myUserId) + ?.membership == Membership.JOIN } - } - } - bobRoomSummariesLive.observeForever(roomJoinedObserver) - bobSession.roomService().joinRoom(aliceRoomId) - } + }, + action = { bobSession.roomService().joinRoom(aliceRoomId) } + ) + // Ensure bob can send messages to the room // val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! // assertNotNull(roomFromBobPOV.powerLevels) @@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } - /** - * @return Alice, Bob and Sam session - */ - fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceAndBobInARoom() - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - - val room = aliceSession.getRoom(aliceRoomId)!! - - val samSession = createSamAccountAndInviteToTheRoom(room) - - // wait the initial sync - SystemClock.sleep(1000) - - return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) - } - - /** - * Create Sam account and invite him in the room. He will accept the invitation - * @Return Sam session - */ - fun createSamAccountAndInviteToTheRoom(room: Room): Session { - val samSession = testHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) - - testHelper.runBlockingTest { - room.membershipService().invite(samSession.myUserId, null) - } - - testHelper.runBlockingTest { - samSession.roomService().joinRoom(room.roomId, null, emptyList()) - } - - return samSession - } - /** * @return Alice and Bob sessions */ - fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { val cryptoTestData = doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -235,49 +165,20 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return cryptoTestData } - private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) - if (andCanDecrypt) { - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } else { - timeLineEvent != null - } + private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { + testHelper.retryPeriodically { + val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) + if (andCanDecrypt) { + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } else { + timeLineEvent != null } } } - fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { - assertEquals(EventType.ENCRYPTED, event.type) - assertNotNull(event.content) - - val eventWireContent = event.content.toContent() - assertNotNull(eventWireContent) - - assertNull(eventWireContent["body"]) - assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"]) - - assertNotNull(eventWireContent["ciphertext"]) - assertNotNull(eventWireContent["session_id"]) - assertNotNull(eventWireContent["sender_key"]) - - assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"]) - - assertNotNull(event.eventId) - assertEquals(roomId, event.roomId) - assertEquals(EventType.MESSAGE, event.getClearType()) - // TODO assertTrue(event.getAge() < 10000) - - val eventContent = event.toContent() - assertNotNull(eventContent) - assertEquals(clearMessage, eventContent["body"]) - assertEquals(senderSession.myUserId, event.senderId) - } - - fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { return MegolmBackupAuthData( publicKey = "abcdefg", signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) @@ -292,44 +193,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { ) } - fun createDM(alice: Session, bob: Session): String { - var roomId: String = "" - testHelper.waitWithLatch { latch -> - roomId = alice.roomService().createDirectRoom(bob.myUserId) - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (t?.any { it.roomId == roomId }.orFalse()) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - } - - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (bob.getRoom(roomId) - ?.membershipService() - ?.getRoomMember(bob.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - bob.roomService().joinRoom(roomId) - } + suspend fun createDM(alice: Session, bob: Session): String { + var roomId = "" + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { it.any { it.roomId == roomId }.orFalse() } + }, + action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) } + ) + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { + bob.getRoom(roomId) + ?.membershipService() + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN + } + }, + action = { bob.roomService().joinRoom(roomId) } + ) return roomId } - fun initializeCrossSigning(session: Session) { - testHelper.doSync { + suspend fun initializeCrossSigning(session: Session) { + testHelper.waitForCallback { session.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -350,57 +242,55 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * Initialize cross-signing, set up megolm backup and save all in 4S */ - fun bootstrapSecurity(session: Session) { + suspend 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) + 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(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) - ssssService.storeSecret( - SELF_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + // set up megolm backup + val creationInfo = testHelper.waitForCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = testHelper.waitForCallback { + 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( - USER_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + KEYBACKUP_SECRET_SSSS_NAME, + secret, listOf(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(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) - } } } - fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) @@ -415,30 +305,26 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { roomId = roomId ).transactionId - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { - it.requestInfo?.fromDevice == alice.sessionParams.deviceId - } != null - } + testHelper.retryPeriodically { + 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!!) + bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, 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 - } + testHelper.retryPeriodically { + val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) + .firstOrNull { it.localId == localId } + if (outgoingRequest?.isReady == true) { + requestID = outgoingRequest.transactionId!! + true + } else { + false } } @@ -454,23 +340,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { var alicePovTx: OutgoingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction - Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx?.state == VerificationTxState.ShortCodeReady } // 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() - } - bobPovTx?.state == VerificationTxState.ShortCodeReady + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() } + bobPovTx?.state == VerificationTxState.ShortCodeReady } assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) @@ -478,38 +360,30 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { bobPovTx!!.userHasVerifiedShortCode() alicePovTx!!.userHasVerifiedShortCode() - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) } - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId) } } - fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { + suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) val room = aliceSession.getRoom(roomId)!! - testHelper.runBlockingTest { - room.roomCryptoService().enableEncryption() - } + room.roomCryptoService().enableEncryption() val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { val session = testHelper.createAccount("User_$index", defaultSessionParams) - testHelper.runBlockingTest(timeout = 600_000) { room.membershipService().invite(session.myUserId, null) } + room.membershipService().invite(session.myUserId, null) println("TEST -> " + session.myUserId + " invited") - testHelper.runBlockingTest { session.roomService().joinRoom(room.roomId, null, emptyList()) } + session.roomService().joinRoom(room.roomId, null, emptyList()) println("TEST -> " + session.myUserId + " joined") sessions.add(session) } @@ -517,47 +391,43 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(roomId, sessions) } - fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { + suspend 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 - } + testHelper.retryPeriodically { + val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root + ?: return@retryPeriodically false + 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, + isSafe = result.isSafe + ) } - Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel()?.body + } 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) { + suspend 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) - } + 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/TestExtensions.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt new file mode 100644 index 0000000000..8f89d42ac0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt @@ -0,0 +1,67 @@ +/* + * 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.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.resume + +suspend fun T.onMain(block: T.() -> R): R { + return withContext(Dispatchers.Main) { + block(this@onMain) + } +} + +suspend fun LiveData.first(timeout: Long = TestConstants.timeOutMillis, predicate: (T) -> Boolean): T { + return wrapWithTimeout(timeout) { + withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val observer = object : Observer { + override fun onChanged(data: T) { + if (predicate(data)) { + removeObserver(this) + continuation.resume(data) + } + } + } + observeForever(observer) + continuation.invokeOnCancellation { removeObserver(observer) } + } + } + } +} + +suspend fun waitFor(continueWhen: suspend () -> T, action: suspend () -> Unit) { + coroutineScope { + val deferred = async { continueWhen() } + action() + deferred.await() + } +} + +suspend fun wrapWithTimeout(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { + val deferred = coroutineScope { + async { block() } + } + return withTimeout(timeout) { deferred.await() } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt index a48b45a1f5..4e1efbb700 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -46,30 +46,26 @@ class DecryptRedactedEventTest : InstrumentedTest { roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) // get the event from bob - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true } val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! - testHelper.runBlockingTest { - try { - val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") - Assert.assertEquals( - "Unexpected redacted reason", - redactionReason, - result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason") - ) - Assert.assertEquals( - "Unexpected Redacted event id", - timelineEvent.eventId, - result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts - ) - } catch (failure: Throwable) { - Assert.fail("Should not throw when decrypting a redacted event") - } + try { + val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") + Assert.assertEquals( + "Unexpected redacted reason", + redactionReason, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason") + ) + Assert.assertEquals( + "Unexpected Redacted event id", + timelineEvent.eventId, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts + ) + } catch (failure: Throwable) { + Assert.fail("Should not throw when decrypting a redacted event") } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt index 32d63a1934..cbbc4dc74e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -40,7 +40,6 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestData import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -57,18 +56,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -81,9 +76,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) // Let alice invite bob - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(bobSession.myUserId) - } + roomAlice.membershipService().invite(bobSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) @@ -114,9 +107,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(samSession.myUserId) - } + roomAlice.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) @@ -135,7 +126,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(false) @@ -162,7 +153,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) @@ -186,7 +177,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) } - private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple, List, Session> { + private suspend fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple, List, Session> { val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1) val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1) @@ -195,9 +186,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let bob invite sam - commonTestHelper.runBlockingTest { - bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) - } + bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId) return Triple(fromAliceNotSharable, fromBobSharable, samSession) @@ -209,18 +198,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -232,18 +217,15 @@ class E2EShareKeysConfigTest : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val keysBackupService = aliceSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = commonTestHelper.doSync { + val megolmBackupCreationInfo = commonTestHelper.waitForCallback { keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = commonTestHelper.doSync { + val version = commonTestHelper.waitForCallback { keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } - commonTestHelper.waitWithLatch { latch -> - keysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) + commonTestHelper.waitForCallback { + keysBackupService.backupAllGroupSessions(null, it) } // signout @@ -253,11 +235,11 @@ class E2EShareKeysConfigTest : InstrumentedTest { newAliceSession.cryptoService().enableShareKeyOnInvite(true) newAliceSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = commonTestHelper.doSync { + val keyVersionResult = commonTestHelper.waitForCallback { kbs.getVersion(version.version, it) } - val importedResult = commonTestHelper.doSync { + val importedResult = commonTestHelper.waitForCallback { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -276,9 +258,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) - } + newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt new file mode 100644 index 0000000000..8b12092b79 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt @@ -0,0 +1,132 @@ +/* + * 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 androidx.test.filters.LargeTest +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeConfigTest : InstrumentedTest { + + @Test + fun testBlacklistUnverifiedDefault() = runCryptoTest(context()) { cryptoTestHelper, _ -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestData.firstSession.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false + cryptoTestData.firstSession.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false + cryptoTestData.secondSession!!.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false + cryptoTestData.secondSession!!.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false + } + + @Test + fun testCantDecryptIfGlobalUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null + } + + cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId) + } + + @Test + fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession) + cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!) + + cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId) + + cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null + } + + cryptoTestHelper.ensureCanDecrypt( + listOf(sentMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + listOf(sentMessage.getLastMessageContent()!!.body) + ) + } + + @Test + fun testCantDecryptIfPerRoomUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null + } + + cryptoTestHelper.ensureCanDecrypt( + listOf(beforeMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + listOf(beforeMessage.getLastMessageContent()!!.body) + ) + + cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true) + + val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + + // ensure received + testHelper.retryPeriodically { + cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null + } + + cryptoTestHelper.ensureCannotDecrypt( + listOf(afterMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + MXCryptoError.ErrorType.KEYS_WITHHELD + ) + } +} 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 f883295495..a36ba8ac02 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 @@ -18,20 +18,28 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.filters.LargeTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 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.crypto.MXCryptoConfig 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 @@ -45,7 +53,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic 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.room.Room @@ -57,12 +64,12 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest -import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.mustFail -import java.util.concurrent.CountDownLatch +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") @RunWith(JUnit4::class) @@ -70,8 +77,6 @@ import java.util.concurrent.CountDownLatch @LargeTest class E2eeSanityTests : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) - /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -104,10 +109,8 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "All accounts created") // we want to invite them in the room otherAccounts.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } // All user should accept invite @@ -129,13 +132,12 @@ class E2eeSanityTests : InstrumentedTest { // All should be able to decrypt 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 - } + testHelper.retryPeriodically { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE && + timeLineEvent.root.mxDecryptionResult?.isSafe == true } } @@ -146,10 +148,8 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } newAccount.forEach { @@ -159,21 +159,17 @@ class E2eeSanityTests : InstrumentedTest { ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) // wait a bit - testHelper.runBlockingTest { - delay(3_000) - } + delay(3_000) // check that messages are encrypted (uisi) newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { - Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED && - timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { + Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.ENCRYPTED && + timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID } } @@ -185,15 +181,13 @@ class E2eeSanityTests : InstrumentedTest { // new members should be able to decrypt it newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { - Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.MESSAGE && - secondMessage == timelineEvent.root.getClearContent().toModel()?.body + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { + Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.MESSAGE && + secondMessage == timelineEvent.root.getClearContent().toModel()?.body } } } @@ -229,10 +223,10 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val bobKeysBackupService = bobSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = testHelper.doSync { + val version = testHelper.waitForCallback { bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } Log.v("#E2E TEST", "... Key backup started and enabled for bob") @@ -248,32 +242,21 @@ class E2eeSanityTests : InstrumentedTest { 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 - } + testHelper.retryPeriodically { + 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 aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - - testHelper.runBlockingTest { - delay(1_000) - } } Log.v("#E2E TEST", "Bob received all and can decrypt") // Let's wait a bit to be sure that bob has backed up the session Log.v("#E2E TEST", "Force key backup for Bob...") - testHelper.waitWithLatch { latch -> - bobKeysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) - } + testHelper.waitForCallback { bobKeysBackupService.backupAllGroupSessions(null, it) } Log.v("#E2E TEST", "... Key backup done for Bob") // Now lets logout both alice and bob to ensure that we won't have any gossiping @@ -284,9 +267,7 @@ class E2eeSanityTests : InstrumentedTest { testHelper.signOutAndClose(bobSession) Log.v("#E2E TEST", "..Logout alice and bob...") - testHelper.runBlockingTest { - delay(1_000) - } + delay(1_000) // Create a new session for bob Log.v("#E2E TEST", "Create a new session for Bob") @@ -295,14 +276,11 @@ class E2eeSanityTests : InstrumentedTest { // check that bob can't currently decrypt Log.v("#E2E TEST", "check that bob can't currently decrypt") sentEventIds.forEach { sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { - Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED + testHelper.retryPeriodically { + val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { + Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") } + timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED } } // after initial sync events are not decrypted, so we have to try manually @@ -311,11 +289,11 @@ class E2eeSanityTests : InstrumentedTest { // Let's now import keys from backup newBobSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = testHelper.doSync { + val keyVersionResult = testHelper.waitForCallback { kbs.getVersion(version.version, it) } - val importedResult = testHelper.doSync { + val importedResult = testHelper.waitForCallback { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -331,6 +309,13 @@ class E2eeSanityTests : InstrumentedTest { // ensure bob can now decrypt cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + + // Check key trust + sentEventIds.forEach { sentEventId -> + val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!! + val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") + assertEquals("Keys from history should be deniable", false, result.isSafe) + } } /** @@ -357,13 +342,11 @@ class E2eeSanityTests : InstrumentedTest { 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 - } + testHelper.retryPeriodically { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -379,10 +362,6 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "check that new bob can't currently decrypt") cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) -// newBobSession.cryptoService().getOutgoingRoomKeyRequests() -// .firstOrNull { -// it.sessionId == -// } // Try to request sentEventIds.forEach { sentEventId -> @@ -390,33 +369,27 @@ class E2eeSanityTests : InstrumentedTest { newBobSession.cryptoService().requestRoomKeyForEvent(event) } - // wait a bit - // 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) - 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 - } - } - } +// sentEventIds.forEach { sentEventId -> +// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! +// .getTimelineEvent(sentEventId)!! +// .root.content.toModel()!!.sessionId +// testHelper.retryPeriodically { +// 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) @@ -438,7 +411,10 @@ class E2eeSanityTests : InstrumentedTest { * Test that if a better key is forwarded (lower index, it is then used) */ @Test - fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun testForwardBetterKey() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession @@ -455,13 +431,11 @@ class E2eeSanityTests : InstrumentedTest { firstMessage.let { 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 - } + testHelper.retryPeriodically { + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -483,13 +457,11 @@ class E2eeSanityTests : InstrumentedTest { secondMessage.let { 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 - } + testHelper.retryPeriodically { + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -503,18 +475,14 @@ class E2eeSanityTests : InstrumentedTest { Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) // Confirm we can decrypt one but not the other - testHelper.runBlockingTest { - mustFail(message = "Should not be able to decrypt event") { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } + mustFail(message = "Should not be able to decrypt event") { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") } - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt event") - } + try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt event") } // Now let's verify bobs session, and re-request keys @@ -533,50 +501,42 @@ class E2eeSanityTests : InstrumentedTest { // old session should have shared the key at earliest known index now // we should be able to decrypt both - 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 + testHelper.retryPeriodically { + val canDecryptFirst = try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false + } + val canDecryptSecond = try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false } + canDecryptFirst && canDecryptSecond } } - private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendService().sendTextMessage(text) + private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { 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 } - .also { list -> - val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } - Log.v("#E2E TEST", "Timeline snapshot is $message") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } + aliceRoomPOV.sendService().sendTextMessage(text) - timeline.dispose() + val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + testHelper.retryPeriodically { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } + Log.v("#E2E TEST", "Timeline snapshot is $message") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null } + timeline.dispose() return sentEventId } @@ -593,106 +553,35 @@ class E2eeSanityTests : InstrumentedTest { 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 - } - } - }) - + val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId) + val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId) // 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) + + val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode) + assertEquals("Decimal code should have matched", oldCode, newCode) // Assert that devices are verified - val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) - val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) + val newDeviceFromOldPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(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.retryPeriodically { + aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null - } + testHelper.retryPeriodically { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null } assertEquals( @@ -725,27 +614,191 @@ class E2eeSanityTests : InstrumentedTest { ) } - private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN + @Test + fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + testHelper.waitForCallback { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + + testHelper.waitForCallback { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) } + }, it) + } + + // add a second session for bob but not cross signed + + val secondBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + // The two bob session should not be able to decrypt any message + + val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!! + Timber.v("#TEST: Send a first message that should be withheld") + val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!! + + // wait for it to be synced back the other side + Timber.v("#TEST: Wait for message to be synced back") + testHelper.retryPeriodically { + bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null + } + + testHelper.retryPeriodically { + secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null + } + + // bob should not be able to decrypt + Timber.v("#TEST: Ensure cannot be decrytped") + cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), bobSession, cryptoTestData.roomId) + cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), secondBobSession, cryptoTestData.roomId) + + // let's try to verify, it should work even if bob devices are untrusted + Timber.v("#TEST: Do the verification") + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) + + Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt") + + val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!! + Timber.v("#TEST: Wait for message to be synced back") + testHelper.retryPeriodically { + bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null + } + + testHelper.retryPeriodically { + secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null + } + + cryptoTestHelper.ensureCanDecrypt(listOf(secondEvent), bobSession, cryptoTestData.roomId, listOf("World")) + cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId) + } + + private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { + return scope.async { + suspendCancellableCoroutine { continuation -> + var oldCode: String? = null + val listener = object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + val readyInfo = pr.readyInfo + if (readyInfo != null) { + beginKeyVerification( + VerificationMethod.SAS, + userId, + 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 -> { + removeListener(this) + // we can release this latch? + continuation.resume(oldCode!!) + } + else -> Unit + } + } + } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } } - 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 suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { + return scope.async { + suspendCancellableCoroutine { continuation -> + var newCode: String? = null + + val listener = object : VerificationService.Listener { + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // let's ready + readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + userId, + 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 -> { + removeListener(this) + continuation.resume(newCode!!) + } + else -> Unit + } + } } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } + } + } + } + + private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + + private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { + sentEventIds.forEach { sentEventId -> + testHelper.retryPeriodically { + 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/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 32a95008b1..91e0026c93 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.wrapWithTimeout @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { */ private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val aliceMessageText = "Hello Bob, I am Alice!" val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId @@ -96,22 +97,21 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") - val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) + val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper) Assert.assertTrue("Message should be sent", aliceMessageId != null) Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") // Bob should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { + timelineEvent.root.getClearType() == EventType.MESSAGE && + timelineEvent.root.mxDecryptionResult?.isSafe == true).also { if (it) { Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } } - } } // Create a new user @@ -121,10 +121,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Aris user created") // Alice invites new user to the room - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") - aliceRoomPOV.membershipService().invite(arisSession.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") + aliceRoomPOV.membershipService().invite(arisSession.myUserId) waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) @@ -137,30 +135,27 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { null -> { // Aris should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + timelineEvent.root.getClearType() == EventType.MESSAGE && + timelineEvent.root.mxDecryptionResult?.isSafe == false ).also { if (it) { Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } } - } } } RoomHistoryVisibility.INVITED, RoomHistoryVisibility.JOINED -> { // Aris should not even be able to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) - ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) - timelineEvent == null - } + testHelper.retryPeriodically { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + timelineEvent == null } } } @@ -238,10 +233,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { private fun testRotationDueToVisibilityChange( initRoomHistoryVisibility: RoomHistoryVisibility, nextRoomHistoryVisibility: RoomHistoryVisibilityContent - ) { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - + ) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId @@ -267,96 +259,84 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { // Bob should be able to decrypt the message var firstAliceMessageMegolmSessionId: String? = null val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) + } + } + } + + Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) + + var secondAliceMessageSessionId: String? = null + sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> + testHelper.retryPeriodically { val timelineEvent = bobRoomPov ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) + ?.getTimelineEvent(secondMessage) (timelineEvent != null && timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) { - firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String Log.v( "#E2E TEST", - "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" ) } } } } - - Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) - - var secondAliceMessageSessionId: String? = null - sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(secondMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - Log.v( - "#E2E TEST", - "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" - ) - } - } - } - } - } assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId) Log.v("#E2E TEST ROTATION", "No rotation needed yet") // Let's change the room history visibility - testHelper.runBlockingTest { - aliceRoomPOV.stateService() - .sendStateEvent( - eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, - stateKey = "", - body = RoomHistoryVisibilityContent( - historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr - ).toContent() - ) - } + aliceRoomPOV.stateService() + .sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + body = RoomHistoryVisibilityContent( + historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr + ).toContent() + ) // ensure that the state did synced down - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content - ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content + ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomVisibility = aliceSession.getRoom(e2eRoomID)!! - .stateService() - .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") - roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + val roomVisibility = aliceSession.getRoom(e2eRoomID)!! + .stateService() + .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") + roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } var aliceThirdMessageSessionId: String? = null sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(thirdMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - } + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(thirdMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String } } } @@ -376,35 +356,34 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { - return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId + private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { + return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let { + Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}") + return it.eventId + } } - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } + private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN } } } - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } + private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") } } } - testHelper.runBlockingTest(60_000) { + wrapWithTimeout(60_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") try { otherSession.roomService().joinRoom(e2eRoomID) @@ -414,11 +393,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } } 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 e8e7b1d708..5c817443ce 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 @@ -52,15 +52,13 @@ class PreShareKeysTest : InstrumentedTest { Log.d("#Test", "Room Key Received from alice $preShareCount") // Force presharing of new outbound key - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - newKeysCount > preShareCount - } + testHelper.retryPeriodically { + val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() + newKeysCount > preShareCount } val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting @@ -85,10 +83,8 @@ class PreShareKeysTest : InstrumentedTest { val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE } } } 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 5fe7376184..889cc9a562 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 @@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBe +import kotlinx.coroutines.suspendCancellableCoroutine +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Before import org.junit.FixMethodOrder @@ -29,6 +30,7 @@ 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.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType @@ -45,7 +47,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.olm.OlmSession import timber.log.Timber -import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -82,7 +83,10 @@ class UnwedgingTest : InstrumentedTest { * -> This is automatically fixed after SDKs restarted the olm session */ @Test - fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun testUnwedging() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -98,69 +102,37 @@ class UnwedgingTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) bobTimeline.start() - val bobFinalLatch = CountDownLatch(1) - val bobHasThreeDecryptedEventsListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } - Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages") - if (decryptedEventReceivedByBob.size == 3) { - if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - bobFinalLatch.countDown() - } - } - } - } - bobTimeline.addListener(bobHasThreeDecryptedEventsListener) - - var latch = CountDownLatch(1) - var bobEventsListener = createEventListener(latch, 1) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() // - Alice sends a 1st message with a 1st megolm session roomFromAlicePOV.sendService().sendTextMessage("First message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1) - messagesReceivedByBob.size shouldBe 1 + messagesReceivedByBob.size shouldBeEqualTo 1 val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! // - Store the olm session between A&B devices // Let us pickle our session with bob here so we can later unpickle it // and wedge our session. val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) - sessionIdsForBob!!.size shouldBe 1 + sessionIdsForBob!!.size shouldBeEqualTo 1 val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! val oldSession = serializeForRealm(olmSession.olmSession) aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - Thread.sleep(6_000) - latch = CountDownLatch(1) - bobEventsListener = createEventListener(latch, 2) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() - Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") // - Alice sends a 2nd message with a 2nd megolm session roomFromAlicePOV.sendService().sendTextMessage("Second message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2) - messagesReceivedByBob.size shouldBe 2 + messagesReceivedByBob.size shouldBeEqualTo 2 // Session should have changed val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! Assert.assertNotEquals(firstMessageSession, secondMessageSession) @@ -173,25 +145,18 @@ class UnwedgingTest : InstrumentedTest { bobSession.cryptoService().getMyDevice().identityKey()!! ) olmDevice.clearOlmSessionCache() - Thread.sleep(6_000) // Force new session, and key share aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") + // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + roomFromAlicePOV.sendService().sendTextMessage("Third message") + // Bob should not be able to decrypt, because the session key could not be sent // Wait for the message to be received by Bob - testHelper.waitWithLatch { - bobEventsListener = createEventListener(it, 3) - bobTimeline.addListener(bobEventsListener) - messagesReceivedByBob = emptyList() - - Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") - // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - roomFromAlicePOV.sendService().sendTextMessage("Third message") - // Bob should not be able to decrypt, because the session key could not be sent - } - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3) - messagesReceivedByBob.size shouldBe 3 + messagesReceivedByBob.size shouldBeEqualTo 3 val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") @@ -201,11 +166,11 @@ class UnwedgingTest : InstrumentedTest { Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged - testHelper.await(bobFinalLatch) - bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) + + Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // It's a trick to force key request on fail to decrypt - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -223,24 +188,22 @@ class UnwedgingTest : InstrumentedTest { } // Wait until we received back the key - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - // we should get back the key and be able to decrypt - val result = testHelper.runBlockingTest { - tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") - } - } - Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") - result != null + testHelper.retryPeriodically { + // we should get back the key and be able to decrypt + val result = tryOrNull { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") } + Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") + result != null } bobTimeline.dispose() } +} - private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { - return object : Timeline.Listener { +private suspend fun Timeline.waitForMessages(expectedCount: Int): List { + return suspendCancellableCoroutine { continuation -> + val listener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { // noop } @@ -250,12 +213,16 @@ class UnwedgingTest : InstrumentedTest { } override fun onTimelineUpdated(snapshot: List) { - messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED } - if (messagesReceivedByBob.size == expectedNumberOfMessages) { - latch.countDown() + if (messagesReceived.size == expectedCount) { + removeListener(this) + continuation.resume(messagesReceived) } } } + + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } 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 ef3fdfeeda..c4fb896934 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 @@ -25,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -42,20 +41,20 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -@Ignore class XSigningTest : InstrumentedTest { @Test fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { @@ -101,14 +100,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(bobAuthParams) @@ -117,7 +116,7 @@ class XSigningTest : InstrumentedTest { } // Check that alice can see bob keys - testHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) @@ -154,14 +153,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(bobAuthParams) @@ -171,12 +170,12 @@ class XSigningTest : InstrumentedTest { // Check that alice can see bob keys val bobUserId = bobSession.myUserId - testHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - testHelper.doSync { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } // Now bobs logs in on a new device and verifies it // We will want to test that in alice POV, this new device would be trusted by cross signing @@ -185,7 +184,7 @@ class XSigningTest : InstrumentedTest { val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login - val data = testHelper.doSync> { + val data = testHelper.waitForCallback> { bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } @@ -197,12 +196,12 @@ class XSigningTest : InstrumentedTest { assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) } // Now alice should cross trust bob's second device - val data2 = testHelper.doSync> { + val data2 = testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } @@ -214,4 +213,104 @@ class XSigningTest : InstrumentedTest { val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) } + + @Test + fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + testHelper.waitForCallback { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + testHelper.waitForCallback { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) + } + + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) + + testHelper.retryPeriodically { + aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + testHelper.retryPeriodically { + aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId).isVerified() + } + + aliceSession.cryptoService() + // Ensure also that bob device is trusted + testHelper.retryPeriodically { + val deviceInfo = aliceSession.cryptoService().getUserDevices(bobSession.myUserId).firstOrNull() + Timber.v("#TEST device:${deviceInfo?.shortDebugString()} trust ${deviceInfo?.trustLevel}") + deviceInfo?.trustLevel?.crossSigningVerified == true + } + + val currentBobMSK = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId)!! + .masterKey()!!.unpaddedBase64PublicKey!! + + testHelper.waitForCallback { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) + } + + testHelper.retryPeriodically { + val newBobMsk = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId) + ?.masterKey()?.unpaddedBase64PublicKey + newBobMsk != null && newBobMsk != currentBobMSK + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + // assert that bob is not trusted anymore from alice s + testHelper.retryPeriodically { + val trust = aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId) + !trust.isVerified() + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.retryPeriodically { + val info = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + info?.wasTrustedOnce == true + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.retryPeriodically { + !aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + // Ensure also that bob device are not trusted + testHelper.retryPeriodically { + aliceSession.cryptoService().getUserDevices(bobSession.myUserId).first().trustLevel?.crossSigningVerified != true + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index 5f26fda946..42a04dbe3f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test @@ -34,54 +34,59 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestHelper -import java.util.concurrent.CountDownLatch +import org.matrix.android.sdk.common.waitFor +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class EncryptionTest : InstrumentedTest { @Test - fun test_EncryptionEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room -> - // Send an encryption Event as an Event (and not as a state event) - room.sendService().sendEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } + fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room -> + // Send an encryption Event as an Event (and not as a state event) + room.sendService().sendEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } @Test - fun test_EncryptionStateEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room -> - runBlocking { - // Send an encryption Event as a State Event - room.stateService().sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = "", - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } - } + fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room -> + // Send an encryption Event as a State Event + room.stateService().sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } - private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) { + private suspend fun performTest(cryptoTestHelper: CryptoTestHelper, roomShouldBeEncrypted: Boolean, action: suspend (Room) -> Unit) { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false) - val aliceSession = cryptoTestData.firstSession val room = aliceSession.getRoom(cryptoTestData.roomId)!! room.roomCryptoService().isEncrypted() shouldBe false val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) - val latch = CountDownLatch(1) + timeline.start() + waitFor( + continueWhen = { timeline.waitForEncryptedMessages() }, + action = { action.invoke(room) } + ) + timeline.dispose() + + room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted + } +} + +private suspend fun Timeline.waitForEncryptedMessages() { + suspendCancellableCoroutine { continuation -> val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest { .filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION } if (newMessages.isNotEmpty()) { - timeline.removeListener(this) - latch.countDown() + removeListener(this) + continuation.resume(Unit) } } } - timeline.start() - timeline.addListener(timelineListener) - - action.invoke(room) - testHelper.await(latch) - timeline.dispose() - testHelper.waitWithLatch { - room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted - it.countDown() - } + addListener(timelineListener) + continuation.invokeOnCancellation { removeListener(timelineListener) } } } 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 7bb53e139c..8e001b84d3 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 @@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Assert.assertNull import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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 @@ -43,7 +44,6 @@ 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.timeline.getLastMessageContent import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.mustFail @@ -51,26 +51,23 @@ import org.matrix.android.sdk.mustFail @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest -@Ignore class KeyShareTests : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) + // @get:Rule val rule = RetryTestRule(3) @Test fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") + Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom( - CreateRoomParams().apply { - visibility = RoomDirectoryVisibility.PRIVATE - enableEncryption() - } - ) - } + val roomId = aliceSession.roomService().createRoom( + CreateRoomParams().apply { + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() + } + ) val room = aliceSession.getRoom(roomId) assertNotNull(room) Thread.sleep(4_000) @@ -86,7 +83,7 @@ class KeyShareTests : InstrumentedTest { aliceSession2.cryptoService().enableKeyGossiping(false) commonTestHelper.syncSession(aliceSession2) - Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") + Log.v("#TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") val roomSecondSessionPOV = aliceSession2.getRoom(roomId) @@ -94,10 +91,8 @@ class KeyShareTests : InstrumentedTest { assertNotNull(receivedEvent) assert(receivedEvent!!.isEncrypted()) - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -111,17 +106,15 @@ class KeyShareTests : InstrumentedTest { var outGoingRequestId: String? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession2.cryptoService().getOutgoingRoomKeyRequests() - .let { - val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } - outGoingRequestId = outgoing?.requestId - outgoing != null - } - } + commonTestHelper.retryPeriodically { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } } - Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") + Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId") val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -131,52 +124,62 @@ class KeyShareTests : InstrumentedTest { // The first session should see an incoming request // the request should be refused, because the device is not trusted - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS -// 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.retryPeriodically { + // DEBUG LOGS + 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", "=========================") } - } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceSession2.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 incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null + } - 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 + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceSession2.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 = 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 } - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } // Mark the device as trusted + + Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}") + val aliceSecondSession = aliceSession2.cryptoService().getMyDevice() + Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}") + aliceSession.cryptoService().setDeviceVerification( DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, aliceSession2.sessionParams.deviceId ?: "" ) + // We only accept forwards from trusted session, so we need to trust on other side to + aliceSession2.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession.sessionParams.deviceId ?: "" + ) + + aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true + // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -193,7 +196,10 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareIfWasIntendedToBeShared() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -210,12 +216,10 @@ class KeyShareTests : InstrumentedTest { // As it was share previously alice should accept to reshare bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - 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 - } + commonTestHelper.retryPeriodically { + 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 } } @@ -224,7 +228,10 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) val aliceSession = testData.firstSession @@ -233,27 +240,22 @@ class KeyShareTests : InstrumentedTest { 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 - } + commonTestHelper.retryPeriodically { + 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!! - // 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 - } + commonTestHelper.retryPeriodically { + 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 } } @@ -261,7 +263,10 @@ class KeyShareTests : InstrumentedTest { * Tests that keys reshared with own verified session are done from the earliest known index */ @Test - fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -277,12 +282,10 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.syncSession(aliceNewSession) // 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 - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -304,26 +307,22 @@ class KeyShareTests : InstrumentedTest { aliceNewSession.cryptoService().enableKeyGossiping(true) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.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 - } + commonTestHelper.retryPeriodically { + 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 } - 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 - } + commonTestHelper.retryPeriodically { + 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 @@ -333,25 +332,26 @@ class KeyShareTests : InstrumentedTest { aliceSession.cryptoService() .verificationService() .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + aliceNewSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.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 + commonTestHelper.retryPeriodically { + // 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! @@ -363,13 +363,11 @@ class KeyShareTests : InstrumentedTest { ) // 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.retryPeriodically { + 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) @@ -381,7 +379,10 @@ class KeyShareTests : InstrumentedTest { * Tests that we don't cancel a request to early on first forward if the index is not good enough */ @Test - fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_dontCancelToEarly() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -394,12 +395,10 @@ class KeyShareTests : InstrumentedTest { 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 - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -421,6 +420,9 @@ class KeyShareTests : InstrumentedTest { aliceSession.cryptoService() .verificationService() .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + aliceNewSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) // /!\ Stop initial alice session syncing so that it can't reply aliceSession.cryptoService().enableKeyGossiping(false) @@ -430,14 +432,12 @@ class KeyShareTests : InstrumentedTest { 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 - } + commonTestHelper.retryPeriodically { + // 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 } @@ -450,14 +450,12 @@ class KeyShareTests : InstrumentedTest { aliceSession.syncService().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 - } + commonTestHelper.retryPeriodically { + 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 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 0aac4297e4..b55ddbc970 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 @@ -27,6 +27,7 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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 @@ -80,10 +81,8 @@ class WithHeldTests : InstrumentedTest { val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() // await for bob unverified session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null - } + testHelper.retryPeriodically { + bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null } val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! @@ -95,65 +94,60 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - 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) - } - ) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + 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) + } + ) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } // 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 - } + testHelper.retryPeriodically { + 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) val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) - // wait until it's decrypted - ev?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE } // Previous message should still be undecryptable (partially withheld session) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - 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) - }) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + 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) + }) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } } @Test - fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun test_WithHeldNoOlm() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -177,26 +171,22 @@ class WithHeldTests : InstrumentedTest { val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId // await for bob session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null - } + testHelper.retryPeriodically { + bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null } // Previous message should still be undecryptable (partially withheld session) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - 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.NO_OLM.value, technicalMessage) - }) { - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + 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.NO_OLM.value, technicalMessage) + }) { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") } // Ensure that alice has marked the session to be shared with bob @@ -216,10 +206,8 @@ class WithHeldTests : InstrumentedTest { // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null - } + testHelper.retryPeriodically { + bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null } val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( @@ -233,7 +221,10 @@ class WithHeldTests : InstrumentedTest { } @Test - fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun test_WithHeldKeyRequest() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -258,27 +249,21 @@ class WithHeldTests : InstrumentedTest { var sessionId: String? = null // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { - // try to decrypt and force key request - tryOrNull { - testHelper.runBlockingTest { - bobSecondSession.cryptoService().decryptEvent(it.root, "") - } - } + testHelper.retryPeriodically { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { + // try to decrypt and force key request + tryOrNull { + bobSecondSession.cryptoService().decryptEvent(it.root, "") } - sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId - timeLineEvent != null } + sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId + timeLineEvent != null } // Check that bob second session requested the key - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) - wc?.code == WithHeldCode.UNAUTHORISED - } + testHelper.retryPeriodically { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt index cf201611a0..8679cf3c99 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -30,7 +30,7 @@ internal data class KeysBackupScenarioData( val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { cryptoTestData.cleanUp(testHelper) testHelper.signOutAndClose(aliceSession2) } 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 2439119f01..01c03b8001 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 @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Rule @@ -48,9 +48,11 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.common.waitFor +import java.security.InvalidParameterException import java.util.Collections import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -116,7 +118,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(null, null, it) } @@ -133,7 +135,6 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) cryptoTestHelper.initializeCrossSigning(bobSession) @@ -143,14 +144,14 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(null, null, it) } assertFalse(keysBackup.isEnabled()) // Create the version - val version = testHelper.doSync { + val version = testHelper.waitForCallback { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -158,10 +159,10 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Check that it's signed with MSK - val versionResult = testHelper.doSync { + val versionResult = testHelper.waitForCallback { keysBackup.getVersion(version.version, it) } - val trust = testHelper.doSync { + val trust = testHelper.waitForCallback { keysBackup.getKeysBackupTrust(versionResult!!, it) } @@ -257,7 +258,7 @@ class KeysBackupTest : InstrumentedTest { var lastBackedUpKeysProgress = 0 - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { assertEquals(nbOfKeys, total) @@ -299,7 +300,7 @@ class KeysBackupTest : InstrumentedTest { val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo // - Check encryptGroupSession() returns stg - val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } + val keyBackupData = keysBackup.encryptGroupSession(session) assertNotNull(keyBackupData) assertNotNull(keyBackupData!!.sessionData) @@ -334,7 +335,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) // - Restore the e2e backup from the homeserver - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -379,7 +380,7 @@ class KeysBackupTest : InstrumentedTest { // assertTrue(unsentRequest != null || sentRequest != null) // // // - Restore the e2e backup from the homeserver -// val importRoomKeysResult = mTestHelper.doSync { +// val importRoomKeysResult = mTestHelper.doSyncSuspending<> { } { // testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, // testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, // null, @@ -429,7 +430,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, true, @@ -445,14 +446,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -489,7 +490,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the recovery key - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -505,14 +506,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -547,13 +548,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong recovery key - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "Bad recovery key", - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "Bad recovery key", + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -591,7 +592,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the password - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -607,14 +608,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -652,13 +653,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong password - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - badPassword, - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + badPassword, + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -679,26 +680,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong recovery key - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeysWithRecoveryKey( + keysBackupService.keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -718,7 +714,7 @@ class KeysBackupTest : InstrumentedTest { // - Restore the e2e backup with the password val steps = ArrayList() - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -771,26 +767,21 @@ class KeysBackupTest : InstrumentedTest { val wrongPassword = "passw0rd" val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - wrongPassword, - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + wrongPassword, + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -808,7 +799,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) // - Restore the e2e backup with the recovery key. - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -833,26 +824,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "password", - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + "password", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is IllegalStateException) } /** @@ -874,12 +860,12 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Get key backup version from the homeserver - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { keysBackup.getCurrentVersion(it) }.toKeysVersionResult() // - Check the returned KeyBackupVersion is trusted - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { keysBackup.getKeysBackupTrust(keysVersionResult!!, it) } @@ -918,34 +904,39 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) // Wait for keys backup to be finished - val latch0 = CountDownLatch(1) var count = 0 - keysBackup.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (newState == KeysBackupState.ReadyToBackUp) { - count++ - - if (count == 2) { - // Remove itself from the list of listeners - keysBackup.removeListener(this) - - latch0.countDown() + waitFor( + continueWhen = { + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + continuation.resume(Unit) + } + } + } + } + keysBackup.addListener(listener) + continuation.invokeOnCancellation { keysBackup.removeListener(listener) } } - } - } - }) - - // - Make alice back up her keys to her homeserver - keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + action = { + // - Make alice back up her keys to her homeserver + keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + ) assertTrue(keysBackup.isEnabled()) - testHelper.await(latch0) - // - Create a new backup with fake data on the homeserver, directly using the rest client val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo() - testHelper.doSync { + testHelper.waitForCallback { (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -953,9 +944,7 @@ class KeysBackupTest : InstrumentedTest { (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() // - Make alice back up all her keys again - val latch2 = CountDownLatch(1) - keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) - testHelper.await(latch2) + testHelper.waitForCallbackError { keysBackup.backupAllGroupSessions(null, it) } // -> That must fail and her backup state must be WrongBackUpVersion assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) @@ -991,7 +980,7 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Wait for keys backup to finish by asking again to backup keys. - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(null, it) } @@ -1016,19 +1005,7 @@ class KeysBackupTest : InstrumentedTest { val stateObserver2 = StateObserver(keysBackup2) - var isSuccessful = false - val latch2 = CountDownLatch(1) - keysBackup2.backupAllGroupSessions( - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: Unit) { - isSuccessful = true - super.onSuccess(data) - } - }) - testHelper.await(latch2) - - assertFalse(isSuccessful) + testHelper.waitForCallbackError { keysBackup2.backupAllGroupSessions(null, it) } // Backup state must be NotTrusted assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) @@ -1042,24 +1019,25 @@ class KeysBackupTest : InstrumentedTest { ) // -> Backup should automatically enable on the new device - val latch4 = CountDownLatch(1) - keysBackup2.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { - // Remove itself from the list of listeners - keysBackup2.removeListener(this) - - latch4.countDown() + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + continuation.resume(Unit) + } } } - }) - testHelper.await(latch4) + keysBackup2.addListener(listener) + continuation.invokeOnCancellation { keysBackup2.removeListener(listener) } + } // -> It must use the same backup version assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) - testHelper.doSync { + testHelper.waitForCallback { aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) } @@ -1092,7 +1070,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Delete the backup - testHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + testHelper.waitForCallback { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } // Backup is now disabled assertFalse(keysBackup.isEnabled()) 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 2cc2b506b9..10abf93bcb 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 @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session @@ -29,7 +30,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.assertDictEquals import org.matrix.android.sdk.common.assertListEquals import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume internal class KeysBackupTestHelper( private val testHelper: CommonTestHelper, @@ -47,7 +48,7 @@ internal class KeysBackupTestHelper( * * @param password optional password */ - fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + suspend fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() waitForKeybackUpBatching() @@ -64,7 +65,7 @@ internal class KeysBackupTestHelper( var lastProgress = 0 var lastTotal = 0 - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { lastProgress = progress @@ -97,13 +98,13 @@ internal class KeysBackupTestHelper( ) } - fun prepareAndCreateKeysBackupData( + suspend fun prepareAndCreateKeysBackupData( keysBackup: KeysBackupService, password: String? = null ): PrepareKeysBackupDataResult { val stateObserver = StateObserver(keysBackup) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(password, null, it) } @@ -112,7 +113,7 @@ internal class KeysBackupTestHelper( Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) // Create the version - val keysVersion = testHelper.doSync { + val keysVersion = testHelper.waitForCallback { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -129,25 +130,26 @@ internal class KeysBackupTestHelper( * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the * KeysBackup object to be in the specified state */ - fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + suspend fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { // If already in the wanted state, return - if (session.cryptoService().keysBackupService().getState() == state) { + val keysBackupService = session.cryptoService().keysBackupService() + if (keysBackupService.getState() == state) { return } // Else observe state changes - val latch = CountDownLatch(1) - - session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - if (newState == state) { - session.cryptoService().keysBackupService().removeListener(this) - latch.countDown() + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + keysBackupService.removeListener(this) + continuation.resume(Unit) + } } } - }) - - testHelper.await(latch) + keysBackupService.addListener(listener) + continuation.invokeOnCancellation { keysBackupService.removeListener(listener) } + } } fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt index 53cf802b91..0dfecffbde 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -58,18 +58,16 @@ class ReplayAttackTest : InstrumentedTest { val fakeEventWithTheSameIndex = sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId)) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - // Lets decrypt the fake event that will have the same message index - val exception = assertFailsWith { - // An exception should be thrown while the same index would have been used for the previous decryption - aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) - } - assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + // Lets decrypt the fake event that will have the same message index + val exception = assertFailsWith { + // An exception should be thrown while the same index would have been used for the previous decryption + aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) } + assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) cryptoTestData.cleanUp(testHelper) } @@ -93,17 +91,15 @@ class ReplayAttackTest : InstrumentedTest { Assert.assertTrue("Message should be sent", sentEvents.size == 1) assertEquals(sentEvents.size, 1) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + try { + // Lets try to decrypt the same event aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - try { - // Lets try to decrypt the same event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - } catch (ex: Throwable) { - fail("Shouldn't throw a decryption error for same event") - } + } catch (ex: Throwable) { + fail("Shouldn't throw a decryption error for same event") } } } 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 c8be6aae74..0467d082a3 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 @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.ssss -import androidx.lifecycle.Observer import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -37,12 +36,12 @@ import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo -import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService @RunWith(AndroidJUnit4::class) @@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - var accountData: UserAccountDataEvent? = null // Assert Account data is updated - testHelper.waitWithLatch { - val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") - val accountDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { - accountData = t.getOrNull() - } - it.countDown() - } - liveAccountData.observeForever(accountDataObserver) - } + val accountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") } + .first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" } + .getOrNull() + assertNotNull("Key should be stored in account data", accountData) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) assertNotNull("Key Content cannot be parsed", parsed) @@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest { assertEquals("Unexpected key name", "Test Key", parsed.name) assertNull("Key was not generated from passphrase", parsed.passphrase) - var defaultKeyAccountData: UserAccountDataEvent? = null + quadS.setDefaultKey(TEST_KEY_ID) + val defaultKeyAccountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } + .first { it.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID } + .getOrNull() + // Set as default key - testHelper.waitWithLatch { latch -> - quadS.setDefaultKey(TEST_KEY_ID) - val liveDefAccountData = - aliceSession.accountDataService().getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) - val accountDefDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { - defaultKeyAccountData = t.getOrNull()!! - latch.countDown() - } - } - liveDefAccountData.observeForever(accountDefDataObserver) - } assertNotNull(defaultKeyAccountData?.content) assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) @@ -112,21 +96,19 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId = "My.Key" - val info = generatedSecret(testHelper, aliceSession, keyId, true) + val info = generatedSecret(aliceSession, keyId, true) val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) // Store a secret val clearSecret = "42".toByteArray().toBase64NoPadding() - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "secret.of.life", - clearSecret, - listOf(KeyRef(null, keySpec)) // default key - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "secret.of.life", + clearSecret, + listOf(KeyRef(null, keySpec)) // default key + ) - val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life") + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> assertNotNull("Element should be encrypted", encryptedContent) @@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest { // Try to decrypt?? - val decryptedSecret = testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "secret.of.life", - null, // default key - keySpec!! - ) - } + val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret( + "secret.of.life", + null, // default key + keySpec!! + ) assertEquals("Secret mismatch", clearSecret, decryptedSecret) } @@ -159,14 +139,10 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) // Test that we don't need to wait for an account data sync to access directly the keyid from DB - testHelper.runBlockingTest { - quadS.setDefaultKey(TEST_KEY_ID) - } + quadS.setDefaultKey(TEST_KEY_ID) } @Test @@ -174,22 +150,20 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" - val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true) + val key1Info = generatedSecret(aliceSession, keyId1, true) val keyId2 = "Key2" - val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true) + val key2Info = generatedSecret(aliceSession, keyId2, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf( - KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), - KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) - ) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ) + ) val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret") val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> @@ -200,21 +174,17 @@ class QuadSTests : InstrumentedTest { assertNotNull(encryptedContent?.get(keyId2)) // Assert that can decrypt with both keys - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! - ) - } - - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId2, - RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! - ) - } + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! + ) + + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! + ) } @Test @@ -224,104 +194,84 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val passphrase = "The good pass phrase" - val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true) + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) + ) - testHelper.runBlockingTest { - try { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromPassphrase( - "A bad passphrase", - key1Info.content?.passphrase?.salt ?: "", - key1Info.content?.passphrase?.iterations ?: 0, - null - ) - ) - } catch (throwable: Throwable) { - assert(throwable is SharedSecretStorageError.BadMac) - } - } - - // Now try with correct key - testHelper.runBlockingTest { + try { aliceSession.sharedSecretStorageService().getSecret( "my.secret", keyId1, RawBytesKeySpec.fromPassphrase( - passphrase, + "A bad passphrase", key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, null ) ) + } catch (throwable: Throwable) { + assert(throwable is SharedSecretStorageError.BadMac) } + + // Now try with correct key + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null + ) + ) } - private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent { - var accountData: UserAccountDataEvent? = null - testHelper.waitWithLatch { - val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) - val accountDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == type) { - accountData = t.getOrNull() - it.countDown() - } - } - liveAccountData.observeForever(accountDataObserver) - } + private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountData = session.accountDataService() + .onMain { getLiveUserAccountDataEvent(type) } + .first { it.getOrNull()?.type == type } + .getOrNull() + assertNotNull("Account Data type:$type should be found", accountData) return accountData!! } - private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKey(keyId, null, keyId, emptyKeySigner) - } + val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo } - private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKeyWithPassphrase( - keyId, - keyId, - passphrase, - emptyKeySigner, - null - ) - } + val creationInfo = quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null + ) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo 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 1bffbeeeaa..fd2136edd5 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 @@ -547,23 +547,19 @@ class SASTest : InstrumentedTest { var requestID: String? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - requestID = prAlicePOV?.transactionId - Log.v("TEST", "== alicePOV is $prAlicePOV") - prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId } Log.v("TEST", "== requestID is $requestID") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() - Log.v("TEST", "== prBobPOV is $prBobPOV") - prBobPOV?.transactionId == requestID - } + testHelper.retryPeriodically { + val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID } bobVerificationService.readyPendingVerification( @@ -573,12 +569,10 @@ class SASTest : InstrumentedTest { ) // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - Log.v("TEST", "== prAlicePOV is $prAlicePOV") - prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null } // Start concurrent! @@ -602,20 +596,16 @@ class SASTest : InstrumentedTest { var alicePovTx: SasVerificationTransaction? var bobPovTx: SasVerificationTransaction? - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== alicePovTx is $alicePovTx") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady } // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== bobPovTx is $bobPovTx") - bobPovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady } } } 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 3f22906965..4ecfe5be8f 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 @@ -164,7 +164,7 @@ class VerificationTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! - testHelper.doSync { callback -> + testHelper.waitForCallback { callback -> aliceSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -181,7 +181,7 @@ class VerificationTest : InstrumentedTest { ) } - testHelper.doSync { callback -> + testHelper.waitForCallback { callback -> bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -261,7 +261,11 @@ class VerificationTest : InstrumentedTest { val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) - val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) + val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount( + aliceSessionToVerify.myUserId, + TestConstants.PASSWORD, + defaultSessionParams + ) val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) @@ -286,11 +290,9 @@ class VerificationTest : InstrumentedTest { otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId), ) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) - requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } - } + testHelper.retryPeriodically { + val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) + requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } } testHelper.signOutAndClose(aliceSessionToVerify) 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 59b3b14532..656e00bcbd 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 @@ -17,7 +17,7 @@ package org.matrix.android.sdk.session.room.timeline import androidx.test.filters.LargeTest -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.internal.assertEquals import org.junit.FixMethodOrder import org.junit.Ignore @@ -35,6 +35,9 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.waitFor +import org.matrix.android.sdk.common.wrapWithTimeout +import kotlin.coroutines.resume @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -69,30 +72,36 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30)) bobTimeline.start() - commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { - val listener = object : Timeline.Listener { + waitFor( + continueWhen = { + wrapWithTimeout(timeout = TestConstants.timeOutMillis * 10) { + suspendCancellableCoroutine { continuation -> + val listener = object : Timeline.Listener { - override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if (direction == Timeline.Direction.FORWARDS) { - return + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if (direction == Timeline.Direction.FORWARDS) { + return + } + if (state.hasMoreToLoad && !state.loading) { + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } else if (!state.hasMoreToLoad) { + bobTimeline.removeListener(this) + continuation.resume(Unit) + } + } + } + bobTimeline.addListener(listener) + continuation.invokeOnCancellation { bobTimeline.removeListener(listener) } + } } - if (state.hasMoreToLoad && !state.loading) { - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } else if (!state.hasMoreToLoad) { - bobTimeline.removeListener(this) - it.countDown() - } - } - } - bobTimeline.addListener(listener) - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } + }, + action = { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) } + ) + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) - val onlySentEvents = runBlocking { - bobTimeline.getSnapshot() - } + val onlySentEvents = bobTimeline.getSnapshot() .filter { it.root.isTextMessage() }.filter { 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 7c97426c39..81351523e9 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 @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.search +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Test @@ -43,7 +44,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", limit = 10, includeProfile = true, afterLimit = 0, @@ -61,7 +62,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", roomId = cryptoTestData.roomId, limit = 10, includeProfile = true, @@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest { } } - private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + @Test + fun sendTextMessageAndSearchPartOfItIncompleteWord() { + doTest(expectedNumberOfResult = 0) { cryptoTestData -> + cryptoTestData.firstSession + .searchService() + .search( + searchTerm = "lore", /* incomplete word */ + roomId = cryptoTestData.roomId, + limit = 10, + includeProfile = true, + afterLimit = 0, + beforeLimit = 10, + orderByRecent = true, + nextBatch = null + ) + } + } + + private fun doTest( + expectedNumberOfResult: Int = 2, + block: suspend (CryptoTestData) -> SearchResult, + ) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -85,11 +107,9 @@ class SearchMessagesTest : InstrumentedTest { 2 ) - val data = commonTestHelper.runBlockingTest { - block.invoke(cryptoTestData) - } + val data = block.invoke(cryptoTestData) - assertTrue(data.results?.size == 2) + data.results?.size shouldBeEqualTo expectedNumberOfResult assertTrue( data.results ?.all { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 2cd579df24..df131cc19a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -55,15 +55,11 @@ class SpaceCreationTest : InstrumentedTest { val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" val topic = "A public space for test" - var spaceId: String = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(roomName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(roomName, topic, null, true) - commonTestHelper.waitWithLatch { - commonTestHelper.retryPeriodicallyWithLatch(it) { - session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null - } + commonTestHelper.retryPeriodically { + val roomSummary = session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary() + roomSummary?.name == roomName && roomSummary.topic == topic } val syncedSpace = session.spaceService().getSpace(spaceId) @@ -79,14 +75,12 @@ class SpaceCreationTest : InstrumentedTest { assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) var powerLevelsContent: PowerLevelsContent? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - powerLevelsContent = syncedSpace.asRoom() - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - powerLevelsContent != null - } + commonTestHelper.retryPeriodically { + powerLevelsContent = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + powerLevelsContent != null } assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) @@ -116,19 +110,13 @@ class SpaceCreationTest : InstrumentedTest { val roomName = "My Space" val topic = "A public space for test" - val spaceId: String - runBlocking { - spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) - // wait a bit to let the summary update it self :/ - delay(400) - } + val spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summary update it self :/ + delay(400) // Try to join from bob, it's a public space no need to invite - val joinResult: JoinSpaceResult - runBlocking { - joinResult = bobSession.spaceService().joinSpace(spaceId) - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) @@ -152,43 +140,24 @@ class SpaceCreationTest : InstrumentedTest { val syncedSpace = aliceSession.spaceService().getSpace(spaceId) // create a room - var firstChild: String? = null - commonTestHelper.waitWithLatch { - firstChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "FirstRoom" - this.topic = "Description of first room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val firstChild: String = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "FirstRoom" + this.topic = "Description of first room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - var secondChild: String? = null - commonTestHelper.waitWithLatch { - secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "SecondRoom" - this.topic = "Description of second room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "SecondRoom" + this.topic = "Description of second room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) // Try to join from bob, it's a public space no need to invite - var joinResult: JoinSpaceResult? = null - commonTestHelper.waitWithLatch { - joinResult = bobSession.spaceService().joinSpace(spaceId) - // wait a bit to let the summary update it self :/ - it.countDown() - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) 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 18645fd6d9..abe9af5e38 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 @@ -17,8 +17,6 @@ package org.matrix.android.sdk.session.space import android.util.Log -import androidx.lifecycle.Observer -import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -39,16 +37,17 @@ import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain +import org.matrix.android.sdk.common.waitFor @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -60,40 +59,28 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" val topic = "A public space for test" - var spaceId = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(spaceName, topic, null, true) val syncedSpace = session.spaceService().getSpace(spaceId) - var roomId = "" - commonTestHelper.runBlockingTest { - roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) - } + val roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - syncedSpace!!.addChildren(roomId, viaServers, null, true) - } + syncedSpace!!.addChildren(roomId, viaServers, null, true) - commonTestHelper.runBlockingTest { - session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - } + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents - val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } - parents?.forEach { - Log.d("## TEST", "parent : $it") - } - parents?.size == 1 && - parents.first().roomSummary?.name == spaceName && - canonicalParents?.size == 1 && - canonicalParents.first().roomSummary?.name == spaceName + commonTestHelper.retryPeriodically { + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + parents?.forEach { + Log.d("## TEST", "parent : $it") } + parents?.size == 1 && + parents.first().roomSummary?.name == spaceName && + canonicalParents?.size == 1 && + canonicalParents.first().roomSummary?.name == spaceName } } @@ -169,7 +156,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -178,7 +164,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) /* val spaceBInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -188,7 +173,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -199,22 +183,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // Create orphan rooms - - var orphan1 = "" - commonTestHelper.runBlockingTest { - orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) - } - - var orphan2 = "" - commonTestHelper.runBlockingTest { - orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) - } + val orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) + val orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) @@ -235,15 +209,15 @@ class SpaceHierarchyTest : InstrumentedTest { assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) // Add a non canonical child and check that it does not appear as orphan - commonTestHelper.runBlockingTest { - val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) - spaceA!!.addChildren(a3, viaServers, null, false) - } + val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) + spaceA.addChildren(a3, viaServers, null, false) + + val orphansUpdate = session.roomService().onMain { + getRoomSummariesLive(roomSummaryQueryParams { + spaceFilter = SpaceFilter.OrphanRooms + }) + }.first { it.size == 2 } - Thread.sleep(6_000) - val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams { - spaceFilter = SpaceFilter.OrphanRooms - }) assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size) } @@ -253,7 +227,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -262,7 +235,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -273,16 +245,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // add back A as subspace of C - commonTestHelper.runBlockingTest { - val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) - spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) - } + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) // A -> C -> A @@ -300,7 +268,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( @@ -310,7 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( @@ -323,13 +289,10 @@ class SpaceHierarchyTest : InstrumentedTest { // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( @@ -338,52 +301,39 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.waitWithLatch { latch -> - - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer> { - override fun onChanged(children: List?) { -// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + waitFor( + continueWhen = { + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + children.any { it.name == "C1" } && children.any { it.name == "C2" } } + }, + action = { + // add C as subspace of B + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) } - } - - flatAChildren.observeForever(childObserver) - - // add C as subspace of B - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - - // C1 and C2 should be in flatten child of A now - } + ) + // C1 and C2 should be in flatten child of A now // Test part one of the rooms val bRoomId = spaceBInfo.roomIds.first() - commonTestHelper.waitWithLatch { latch -> - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer> { - override fun onChanged(children: List?) { - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.roomId == bRoomId } == false) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + waitFor( + continueWhen = { + // The room should have disappear from flat children + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + !children.any { it.roomId == bRoomId } } + }, + action = { + // part from b room + session.roomService().leaveRoom(bRoomId) } - } + ) - // The room should have disapear from flat children - flatAChildren.observeForever(childObserver) - // part from b room - session.roomService().leaveRoom(bRoomId) - } commonTestHelper.signOutAndClose(session) } @@ -392,68 +342,57 @@ class SpaceHierarchyTest : InstrumentedTest { val roomIds: List ) - private fun createPublicSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPublicSpace( session: Session, spaceName: String, childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - - roomIds = childInfo.map { entry -> - session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = childInfo.map { entry -> + session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) } - private fun createPrivateSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPrivateSpace( session: Session, spaceName: String, childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - roomIds = - childInfo.map { entry -> - val homeServerCapabilities = session - .homeServerCapabilitiesService() - .getHomeServerCapabilities() - session.roomService().createRoom(CreateRoomParams().apply { - name = entry.first - this.featurePreset = RestrictedRoomPreset( - homeServerCapabilities, - listOf( - RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) - ) - ) - }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + val roomIds = childInfo.map { entry -> + val homeServerCapabilities = session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + session.roomService().createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) @@ -464,7 +403,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -473,7 +411,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -483,7 +420,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -494,10 +430,8 @@ class SpaceHierarchyTest : InstrumentedTest { val viaServers = listOf(session.sessionParams.homeServerHost ?: "") // add C as subspace of B - runBlocking { - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - } + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) // Thread.sleep(4_000) // + A @@ -507,11 +441,9 @@ class SpaceHierarchyTest : InstrumentedTest { // + C // + c1, c2 - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } - rootSpaces.size == 2 - } + commonTestHelper.retryPeriodically { + val rootSpaces = session.spaceService().getRootSpaceSummaries() + rootSpaces.size == 2 } } @@ -521,7 +453,6 @@ class SpaceHierarchyTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val spaceAInfo = createPrivateSpace( - commonTestHelper, aliceSession, "Private Space A", listOf( Triple("General", true /*suggested*/, true/*canonical*/), @@ -529,85 +460,58 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.runBlockingTest { - aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) - } + aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) - commonTestHelper.runBlockingTest { - bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) - } + bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) - var bobRoomId = "" - commonTestHelper.runBlockingTest { - bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) - bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - } + val bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) + bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - commonTestHelper.runBlockingTest { - aliceSession.roomService().joinRoom(bobRoomId) - } + aliceSession.roomService().joinRoom(bobRoomId) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true - } + commonTestHelper.retryPeriodically { + aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true } - commonTestHelper.runBlockingTest { - bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - } + bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) - stateEvent != null - } + commonTestHelper.retryPeriodically { + val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) + stateEvent != null } // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space - commonTestHelper.runBlockingTest { - // we can see the state event - // but it is not valid and room is not in hierarchy - assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) - } + // we can see the state event + // but it is not valid and room is not in hierarchy + assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) // Let's now try to make alice admin of the room - commonTestHelper.waitWithLatch { - val room = bobSession.getRoom(bobRoomId)!! - val currentPLContent = room - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - .toModel() + val room = bobSession.getRoom(bobRoomId)!! + val currentPLContent = room + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + .toModel() - val newPowerLevelsContent = currentPLContent - ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) - ?.toContent() + val newPowerLevelsContent = currentPLContent + ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) + ?.toContent() - room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) - it.countDown() - } + room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?.let { PowerLevelsHelper(it) } - powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) - } + commonTestHelper.retryPeriodically { + val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } + powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) } - commonTestHelper.waitWithLatch { - aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - it.countDown() - } + aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true - } + commonTestHelper.retryPeriodically { + bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true } } @@ -616,7 +520,6 @@ class SpaceHierarchyTest : InstrumentedTest { val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -625,7 +528,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -641,51 +543,39 @@ class SpaceHierarchyTest : InstrumentedTest { val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId) val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId) - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(B1roomId, viaServers, null, true) - } + spaceA!!.addChildren(B1roomId, viaServers, null, true) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) } val newAName = "FooBar" - commonTestHelper.runBlockingTest { - spaceA!!.asRoom().stateService().updateName(newAName) - } + spaceA.asRoom().stateService().updateName(newAName) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(newAName) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(newAName) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(newAName) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(newAName) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 893e90fb3e..7119563617 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -70,4 +70,8 @@ data class MatrixConfiguration( * List of network interceptors, they will be added when building an OkHttp client. */ val networkInterceptors: List = emptyList(), + /** + * Sync configuration. + */ + val syncConfig: SyncConfig = SyncConfig(), ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt new file mode 100644 index 0000000000..a9753e2407 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt @@ -0,0 +1,24 @@ +/* + * 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 + +data class SyncConfig( + /** + * Time to keep sync connection alive for before making another request in milliseconds. + */ + val longPollTimeout: Long = 30_000L, +) 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 20132c4d16..d74ed9a49f 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 @@ -41,7 +41,8 @@ data class MXCryptoConfig constructor( /** * 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` + * You can limit request only to your sessions by turning this setting to `true`. + * Forwarded keys coming from other users will also be ignored if set to true. */ val limitRoomKeyRequestsToMyDevices: Boolean = true, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt index d3f6ec2287..1d6e79c8f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -68,6 +68,11 @@ sealed interface QueryStringValue { */ data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + /** + * The tested field must not contain the [string]. + */ + data class NotContains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + /** * Case enum for [ContentQueryStringValue]. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt index e701e0f3ba..234a8eee98 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt @@ -131,11 +131,10 @@ class SecretStoringUtils @Inject constructor( * * The secret is encrypted using the following method: AES/GCM/NoPadding */ - @SuppressLint("NewApi") @Throws(Exception::class) fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray { return when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias) + buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> encryptBytesM(secret, keyAlias) else -> encryptBytes(secret, keyAlias) } } @@ -156,10 +155,9 @@ class SecretStoringUtils @Inject constructor( } } - @SuppressLint("NewApi") fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> saveSecureObjectM(keyAlias, output, any) else -> saveSecureObject(keyAlias, output, any) } } @@ -189,7 +187,6 @@ class SecretStoringUtils @Inject constructor( return cipher } - @SuppressLint("NewApi") @RequiresApi(Build.VERSION_CODES.M) private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt index a15e73eb88..96dac27618 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt @@ -32,5 +32,13 @@ fun Session.getRoomSummary(roomIdOrAlias: String): RoomSummary? = roomService(). /** * Get a user using the UserService of a Session. + * @param userId the userId to look for. + * @return a user with userId or null if the User is not known yet by the SDK. + * See [org.matrix.android.sdk.api.session.user.UserService.resolveUser] to ensure that a User is retrieved. */ fun Session.getUser(userId: String): User? = userService().getUser(userId) + +/** + * Similar to [getUser], but fallback to a User without details if the User is not known by the SDK, or if Session is null. + */ +fun Session?.getUserOrDefault(userId: String): User = this?.userService()?.getUser(userId) ?: User(userId) 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 e0e662c789..d2aa8020e8 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 @@ -61,6 +61,8 @@ interface CryptoService { fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData + fun setWarnOnUnknownDevices(warn: Boolean) fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) @@ -77,6 +79,8 @@ interface CryptoService { fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + fun getLiveGlobalCryptoConfig(): LiveData + /** * Enable or disable key gossiping. * Default is true. @@ -100,7 +104,7 @@ interface CryptoService { */ fun isShareKeysOnInviteEnabled(): Boolean - fun setRoomUnBlacklistUnverifiedDevices(roomId: String) + fun setRoomUnBlockUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -112,7 +116,7 @@ interface CryptoService { suspend fun exportRoomKeys(password: String): ByteArray - fun setRoomBlacklistUnverifiedDevices(roomId: String) + fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt new file mode 100644 index 0000000000..6405652a68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt @@ -0,0 +1,23 @@ +/* + * 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 + +data class GlobalCryptoConfig( + val globalBlockUnverifiedDevices: Boolean, + val globalEnableKeyGossiping: Boolean, + val enableKeyForwardingOnInvite: Boolean, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt index 9604decd62..30a2cfd719 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning data class MXCrossSigningInfo( val userId: String, - val crossSigningKeys: List + val crossSigningKeys: List, + val wasTrustedOnce: Boolean ) { fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true && diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt index b144069b99..500d016002 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt @@ -52,9 +52,17 @@ data class DeviceInfo( * The last ip address. */ @Json(name = "last_seen_ip") - val lastSeenIp: String? = null + val lastSeenIp: String? = null, + + @Json(name = "org.matrix.msc3852.last_seen_user_agent") + val unstableLastSeenUserAgent: String? = null, + + @Json(name = "last_seen_user_agent") + val lastSeenUserAgent: String? = null, ) : DatedObject { override val date: Long get() = lastSeenTs ?: 0 + + fun getBestLastSeenUserAgent() = lastSeenUserAgent ?: unstableLastSeenUserAgent } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt index 0a0ccc2db3..66d7558fe2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt @@ -43,5 +43,7 @@ data class MXEventDecryptionResult( * List of curve25519 keys involved in telling us about the senderCurve25519Key and * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. */ - val forwardingCurve25519KeyChain: List = emptyList() + val forwardingCurve25519KeyChain: List = emptyList(), + + val isSafe: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt index a26f6606ed..6d57318f87 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt @@ -44,5 +44,10 @@ data class OlmDecryptionResult( /** * Devices which forwarded this session to us (normally empty). */ - @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List? = null + @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List? = null, + + /** + * True if the key used to decrypt is considered safe (trusted). + */ + @Json(name = "key_safety") val isSafe: Boolean? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt new file mode 100644 index 0000000000..e3c7057b6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt @@ -0,0 +1,28 @@ +/* + * 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 + +enum class UserVerificationLevel { + + VERIFIED_ALL_DEVICES_TRUSTED, + + VERIFIED_WITH_DEVICES_UNTRUSTED, + + UNVERIFIED_BUT_WAS_PREVIOUSLY, + + WAS_NEVER_VERIFIED, +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 59dc6c434d..f5d2c0d9a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -174,15 +174,29 @@ data class Event( * @return the event type */ fun getClearType(): String { - return mxDecryptionResult?.payload?.get("type")?.toString() ?: type ?: EventType.MISSING_TYPE + return getDecryptedType() ?: type ?: EventType.MISSING_TYPE + } + + /** + * @return The decrypted type, or null. Won't fallback to the wired type + */ + fun getDecryptedType(): String? { + return mxDecryptionResult?.payload?.get("type")?.toString() } /** * @return the event content */ fun getClearContent(): Content? { + return getDecryptedContent() ?: content + } + + /** + * @return the decrypted event content or null, Won't fallback to the wired content + */ + fun getDecryptedContent(): Content? { @Suppress("UNCHECKED_CAST") - return mxDecryptionResult?.payload?.get("content") as? Content ?: content + return mxDecryptionResult?.payload?.get("content") as? Content } fun toContentStringWithIndent(): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 84c25776e7..3ad4f3a87f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -128,4 +128,17 @@ object EventType { type == CALL_REJECT || type == CALL_REPLACES } + + fun isVerificationEvent(type: String): Boolean { + return when (type) { + KEY_VERIFICATION_START, + KEY_VERIFICATION_ACCEPT, + KEY_VERIFICATION_KEY, + KEY_VERIFICATION_MAC, + KEY_VERIFICATION_CANCEL, + KEY_VERIFICATION_DONE, + KEY_VERIFICATION_READY -> true + else -> false + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 5d2769ac3c..8031fcaeea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService @@ -60,11 +61,22 @@ interface Room { */ fun getRoomSummaryLive(): LiveData> + /** + * A live [LocalRoomSummary] associated with the room. + * You can observe this summary to get dynamic data from this room. + */ + fun getLocalRoomSummaryLive(): LiveData> + /** * A current snapshot of [RoomSummary] associated with the room. */ fun roomSummary(): RoomSummary? + /** + * A current snapshot of [LocalRoomSummary] associated with the room. + */ + fun localRoomSummary(): LocalRoomSummary? + /** * Use this room as a Space, if the type is correct. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index ad8106c9c1..65383f1007 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -117,6 +118,12 @@ interface RoomService { */ fun getRoomSummaryLive(roomId: String): LiveData> + /** + * A live [LocalRoomSummary] associated with the room with id [roomId]. + * You can observe this summary to get dynamic data from this room, even if the room is not joined yet + */ + fun getLocalRoomSummaryLive(roomId: String): LiveData> + /** * Get a snapshot list of room summaries. * @return the immutable list of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 60963ef25a..d651f06e23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -20,8 +20,10 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams.Builder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams /** @@ -52,6 +54,10 @@ fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of this class. */ data class RoomSummaryQueryParams( + /** + * Query for the roomId. + */ + val roomId: QueryStringValue, /** * Query for the displayName of the room. The display name can be the value of the state event, * or a value returned by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider]. @@ -94,6 +100,7 @@ data class RoomSummaryQueryParams( * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of [RoomSummaryQueryParams]. */ class Builder { + var roomId: QueryStringValue = QueryStringValue.NotContains(RoomLocalEcho.PREFIX) var displayName: QueryStringValue = QueryStringValue.NoCondition var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var memberships: List = Membership.all() @@ -104,6 +111,7 @@ data class RoomSummaryQueryParams( var spaceFilter: SpaceFilter = SpaceFilter.NoFilter fun build() = RoomSummaryQueryParams( + roomId = roomId, displayName = displayName, canonicalAlias = canonicalAlias, memberships = memberships, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt new file mode 100644 index 0000000000..4fc99225c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt @@ -0,0 +1,24 @@ +/* + * 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.room.model + +enum class LocalRoomCreationState { + NOT_CREATED, + CREATING, + FAILURE, + CREATED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt new file mode 100644 index 0000000000..eced1dd581 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt @@ -0,0 +1,46 @@ +/* + * 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.room.model + +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +/** + * This class holds some data of a local room. + * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class LocalRoomSummary( + /** + * The roomId of the room. + */ + val roomId: String, + /** + * The room summary of the room. + */ + val roomSummary: RoomSummary?, + /** + * The creation params attached to the room. + */ + val createRoomParams: CreateRoomParams?, + /** + * The roomId of the created room (ie. created on the server), if any. + */ + val replacementRoomId: String?, + /** + * The creation state of the room. + */ + val creationState: LocalRoomCreationState, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 7d3109fb6e..2388bee0ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -34,5 +34,4 @@ data class SpaceChildInfo( val canonicalAlias: String?, val aliases: List?, val worldReadable: Boolean - ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt index 7ef0d63924..ec0e642ad3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt @@ -20,7 +20,7 @@ import java.util.UUID object RoomLocalEcho { - private const val PREFIX = "!local." + const val PREFIX = "!local." /** * Tell whether the provider room id is a local id. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt index ecc3eb5224..d03f4c42cf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.api.session.space -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent data class SpaceHierarchyData( val rootSummary: RoomSummary, val children: List, - val childrenState: List, + val childrenState: List, val nextToken: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index c7a6405014..5d2a9412d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.space import android.net.Uri import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -75,12 +75,12 @@ interface SpaceService { suggestedOnly: Boolean? = null, limit: Int? = null, from: String? = null, - knownStateList: List? = null + knownStateList: List? = null ): SpaceHierarchyData /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of List[SpaceSummary] + * @return the [LiveData] of List[RoomSummary] */ fun getSpaceSummariesLive( queryParams: SpaceSummaryQueryParams, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt new file mode 100644 index 0000000000..13aa0336e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.space.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content + +@JsonClass(generateAdapter = true) +data class SpaceChildSummaryEvent( + @Json(name = "type") val type: String? = null, + @Json(name = "state_key") val stateKey: String? = null, + @Json(name = "content") val content: Content? = null, + @Json(name = "sender") val senderId: String? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt index 0c5465e12a..7075023798 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -29,7 +29,7 @@ interface UserService { /** * Get a user from a userId. * @param userId the userId to look for. - * @return a user with userId or null + * @return a user with userId or null if the User is not known yet by the SDK. See [resolveUser] to ensure that a User is retrieved. */ fun getUser(userId: String): User? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt index 900a2e237f..c8c328c92c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.util +import androidx.annotation.ChecksSdkIntAtLeast + interface BuildVersionSdkIntProvider { /** * Return the current version of the Android SDK. @@ -26,9 +28,13 @@ interface BuildVersionSdkIntProvider { * Checks the if the current OS version is equal or greater than [version]. * @return A `non-null` result if true, `null` otherwise. */ + @ChecksSdkIntAtLeast(parameter = 0, lambda = 1) fun whenAtLeast(version: Int, result: () -> T): T? { return if (get() >= version) { result() } else null } + + @ChecksSdkIntAtLeast(parameter = 0) + fun isAtLeast(version: Int) = get() >= version } 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 8dd7c309c6..9c3e0ba1c5 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 @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.listeners.ProgressListener 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.GlobalCryptoConfig 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 @@ -79,6 +80,7 @@ import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationActio 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.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService @@ -183,7 +185,8 @@ internal class DefaultCryptoService @Inject constructor( private val cryptoCoroutineScope: CoroutineScope, private val eventDecryptor: EventDecryptor, private val verificationMessageProcessor: VerificationMessageProcessor, - private val liveEventManager: Lazy + private val liveEventManager: Lazy, + private val unrequestedForwardManager: UnRequestedForwardManager, ) : CryptoService { private val isStarting = AtomicBoolean(false) @@ -399,6 +402,7 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) incomingKeyRequestManager.close() outgoingKeyRequestManager.close() + unrequestedForwardManager.close() olmDevice.release() cryptoStore.close() } @@ -485,6 +489,14 @@ internal class DefaultCryptoService @Inject constructor( // just for safety but should not throw Timber.tag(loggerTag.value).w("failed to process incoming room key requests") } + + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events -> + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + events.forEach { + onRoomKeyEvent(it, true) + } + } + } } } } @@ -844,10 +856,12 @@ internal class DefaultCryptoService @Inject constructor( * Handle a key event. * * @param event the key event. + * @param acceptUnrequested, if true it will force to accept unrequested keys. */ - private fun onRoomKeyEvent(event: Event) { - val roomKeyContent = event.getClearContent().toModel() ?: return - Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) { + val roomKeyContent = event.getDecryptedContent().toModel() ?: return + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") return @@ -857,7 +871,7 @@ internal class DefaultCryptoService @Inject constructor( Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } - alg.onRoomKeyEvent(event, keysBackupService) + alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested) } private fun onKeyWithHeldReceived(event: Event) { @@ -950,6 +964,15 @@ internal class DefaultCryptoService @Inject constructor( * @param event the membership event causing the change */ private fun onRoomMembershipEvent(roomId: String, event: Event) { + // because the encryption event can be after the join/invite in the same batch + event.stateKey?.let { _ -> + val roomMember: RoomMemberContent? = event.content.toModel() + val membership = roomMember?.membership + if (membership == Membership.INVITE) { + unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis()) + } + } + roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return event.stateKey?.let { userId -> @@ -1141,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getGlobalBlacklistUnverifiedDevices() } + override fun getLiveGlobalCryptoConfig(): LiveData { + return cryptoStore.getLiveGlobalCryptoConfig() + } + /** * Tells whether the client should encrypt messages only for the verified devices * in this room. @@ -1149,39 +1176,28 @@ internal class DefaultCryptoService @Inject constructor( * @param roomId the room id * @return true if the client should encrypt messages only for the verified devices. */ -// TODO add this info in CryptoRoomEntity? override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { - return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) } + return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) } ?: false } /** - * Manages the room black-listing for unverified devices. + * A live status regarding sharing keys for unverified devices in this room. * - * @param roomId the room id - * @param add true to add the room id to the list, false to remove it. + * @return Live status */ - private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { - val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() - - if (add) { - if (roomId !in roomIds) { - roomIds.add(roomId) - } - } else { - roomIds.remove(roomId) - } - - cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { + return cryptoStore.getLiveBlockUnverifiedDevices(roomId) } /** * Add this room to the ones which don't encrypt messages to unverified devices. * * @param roomId the room id + * @param block if true will block sending keys to unverified devices */ - override fun setRoomBlacklistUnverifiedDevices(roomId: String) { - setRoomBlacklistUnverifiedDevices(roomId, true) + override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { + cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) } /** @@ -1189,8 +1205,8 @@ internal class DefaultCryptoService @Inject constructor( * * @param roomId the room id */ - override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { - setRoomBlacklistUnverifiedDevices(roomId, false) + override fun setRoomUnBlockUnverifiedDevices(roomId: String) { + setRoomBlockUnverifiedDevices(roomId, false) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index 39dfb72149..6d197a09ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor( internalStoreGroupSession(new, sessionId, senderKey) } + @Synchronized + fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}") + + store.storeInboundGroupSessions( + listOf( + old.wrapper.copy( + sessionData = old.wrapper.sessionData.copy(trusted = true) + ) + ) + ) + // will release it :/ + sessionCache.remove(CacheKey(sessionId, senderKey)) + } + @Synchronized fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { internalStoreGroupSession(holder, sessionId, senderKey) 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 96ccba51dc..faadf339e9 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto import androidx.annotation.VisibleForTesting import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -602,6 +603,7 @@ internal class MXOlmDevice @Inject constructor( * @param keysClaimed Other keys the sender claims. * @param exportFormat true if the megolm keys are in export format * @param sharedHistory MSC3061, this key is sharable on invite + * @param trusted True if the key is coming from a trusted source * @return true if the operation succeeds. */ fun addInboundGroupSession( @@ -612,7 +614,8 @@ internal class MXOlmDevice @Inject constructor( forwardingCurve25519KeyChain: List, keysClaimed: Map, exportFormat: Boolean, - sharedHistory: Boolean + sharedHistory: Boolean, + trusted: Boolean ): AddSessionResult { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { if (exportFormat) { @@ -620,6 +623,8 @@ internal class MXOlmDevice @Inject constructor( } else { OlmInboundGroupSession(sessionKey) } + } ?: return AddSessionResult.NotImported.also { + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId") } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } @@ -631,31 +636,49 @@ internal class MXOlmDevice @Inject constructor( val existingFirstKnown = tryOrNull { existingSession.session.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?.releaseSession() + candidateSession.releaseSession() // Probably should discard it? } - val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex } - // If our existing session is better we keep it - if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession?.releaseSession() - return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) + val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex } + ?: return AddSessionResult.NotImported.also { + candidateSession.releaseSession() + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index") + } + + val keyConnects = existingSession.session.connects(candidateSession) + if (!keyConnects) { + Timber.tag(loggerTag.value) + .e("## addInboundGroupSession() Unconnected key") + if (!trusted) { + // Ignore the not connecting unsafe, keep existing + Timber.tag(loggerTag.value) + .e("## addInboundGroupSession() Received unsafe unconnected key") + return AddSessionResult.NotImported + } + // else if the new one is safe and does not connect with existing, import the new one + } else { + // If our existing session is better we keep it + if (existingFirstKnown <= newKnownFirstIndex) { + val shouldUpdateTrust = trusted && (existingSession.sessionData.trusted != true) + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : updateTrust for $sessionId") + if (shouldUpdateTrust) { + // the existing as a better index but the new one is trusted so update trust + inboundGroupSessionStore.updateToSafe(existingSessionHolder, sessionId, senderKey) + } + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") + candidateSession.releaseSession() + return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) + } } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession?.releaseSession() + candidateSession.releaseSession() return AddSessionResult.NotImported } } Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - // sanity check on the new session - if (null == candidateSession) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") - return AddSessionResult.NotImported - } - try { if (candidateSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") @@ -674,6 +697,7 @@ internal class MXOlmDevice @Inject constructor( keysClaimed = keysClaimed, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, sharedHistory = sharedHistory, + trusted = trusted ) val wrapper = MXInboundMegolmSessionWrapper( @@ -689,6 +713,16 @@ internal class MXOlmDevice @Inject constructor( return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) } + fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean { + return try { + val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex) + this.export(lowestCommonIndex) == other.export(lowestCommonIndex) + } catch (failure: Throwable) { + // native error? key disposed? + false + } + } + /** * Import an inbound group sessions to the session store. * @@ -821,7 +855,8 @@ internal class MXOlmDevice @Inject constructor( payload, wrapper.sessionData.keysClaimed, senderKey, - wrapper.sessionData.forwardingCurve25519KeyChain + wrapper.sessionData.forwardingCurve25519KeyChain, + isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse() ) } 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 index a79e1a8901..5691f24d17 100644 --- 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 @@ -267,13 +267,24 @@ internal class SecretShareManager @Inject constructor( Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") return } + // no need to download keys, after a verification we already forced download + val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) } + if (sendingDevice == null) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}") + return + } // Was that sent by us? - if (toDevice.senderId != credentials.userId) { + if (sendingDevice.userId != credentials.userId) { Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") return } + if (!sendingDevice.isVerified) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}") + return + } + val secretContent = toDevice.getClearContent().toModel() ?: return val existingRequest = verifMutex.withLock { 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 6847a46369..d9fd5f10ce 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 @@ -41,6 +41,7 @@ internal interface IMXDecrypting { * * @param event the key event. * @param defaultKeysBackupService the keys backup service + * @param forceAccept the keys backup service */ - fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} + fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} } 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 ed9064ee9d..828bf9fec0 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,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.orFalse 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 @@ -34,16 +35,20 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.session.StreamEventsManager +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) internal class MXMegolmDecryption( private val olmDevice: MXOlmDevice, + private val myUserId: String, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val matrixConfiguration: MatrixConfiguration, - private val liveEventManager: Lazy + private val liveEventManager: Lazy, + private val unrequestedForwardManager: UnRequestedForwardManager, + private val cryptoConfig: MXCryptoConfig, + private val clock: Clock, ) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -94,7 +99,8 @@ internal class MXMegolmDecryption( senderCurve25519Key = olmDecryptionResult.senderKey, claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - .orEmpty() + .orEmpty(), + isSafe = olmDecryptionResult.isSafe.orFalse() ).also { liveEventManager.get().dispatchLiveEventDecrypted(event, it) } @@ -182,13 +188,23 @@ internal class MXMegolmDecryption( * * @param event the key event. * @param defaultKeysBackupService the keys backup service + * @param forceAccept if true will force to accept the forwarded key */ - override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { - Timber.tag(loggerTag.value).v("onRoomKeyEvent()") + override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { + Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") var exportFormat = false - val roomKeyContent = event.getClearContent().toModel() ?: return + val roomKeyContent = event.getDecryptedContent()?.toModel() ?: return + + val eventSenderKey: String = event.getSenderKey() ?: return Unit.also { + Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field") + } + + // this device might not been downloaded now? + val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey) + + lateinit var sessionInitiatorSenderKey: String + val trusted: Boolean - var senderKey: String? = event.getSenderKey() var keysClaimed: MutableMap = HashMap() val forwardingCurve25519KeyChain: MutableList = ArrayList() @@ -196,32 +212,25 @@ internal class MXMegolmDecryption( Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") return } - if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + if (event.getDecryptedType() == 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() + val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel() ?: return forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { forwardingCurve25519KeyChain.addAll(it) } - if (senderKey == null) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field") - return - } - - forwardingCurve25519KeyChain.add(senderKey) + forwardingCurve25519KeyChain.add(eventSenderKey) exportFormat = true - senderKey = forwardedRoomKeyContent.senderKey - if (null == senderKey) { + sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also { Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") - return } if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { @@ -230,13 +239,52 @@ internal class MXMegolmDecryption( } keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key - } else { - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - if (null == senderKey) { - Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + + // checking if was requested once. + // should we check if the request is sort of active? + val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest( + roomId = forwardedRoomKeyContent.roomId.orEmpty(), + sessionId = forwardedRoomKeyContent.sessionId.orEmpty(), + algorithm = forwardedRoomKeyContent.algorithm.orEmpty(), + senderKey = forwardedRoomKeyContent.senderKey.orEmpty(), + ).isEmpty() + + trusted = false + + if (!forceAccept && wasNotRequested) { +// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty() + unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis()) + // Ignore unsolicited + Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested") + return + } + + // Check who sent the request, as we requested we have the device keys (no need to download) + val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey) + if (sessionThatIsSharing == null) { + Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey") return } + val isOwnDevice = myUserId == sessionThatIsSharing.userId + val isDeviceVerified = sessionThatIsSharing.isVerified + val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey + + val isLegitForward = (isOwnDevice && isDeviceVerified) || + (!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator) + val shouldAcceptForward = forceAccept || isLegitForward + + if (!shouldAcceptForward) { + Timber.tag(loggerTag.value) + .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," + + " fromInitiator:$isFromSessionInitiator") + return + } + } else { + // It's a m.room_key so safe + trusted = true + sessionInitiatorSenderKey = eventSenderKey + Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") // inherit the claimed ed25519 key from the setup message keysClaimed = event.getKeysClaimed().toMutableMap() } @@ -246,12 +294,15 @@ internal class MXMegolmDecryption( sessionId = roomKeyContent.sessionId, sessionKey = roomKeyContent.sessionKey, roomId = roomKeyContent.roomId, - senderKey = senderKey, + senderKey = sessionInitiatorSenderKey, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, keysClaimed = keysClaimed, exportFormat = exportFormat, - sharedHistory = roomKeyContent.getSharedKey() - ) + sharedHistory = roomKeyContent.getSharedKey(), + trusted = trusted + ).also { + Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it") + } when (addSessionResult) { is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex @@ -259,35 +310,28 @@ internal class MXMegolmDecryption( 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, + senderKey = sessionInitiatorSenderKey, fromIndex = index, - fromDevice = fromDevice, + fromDevice = fromDevice?.deviceId, event = event ) cryptoStore.saveIncomingForwardKeyAuditTrail( roomId = roomKeyContent.roomId, sessionId = roomKeyContent.sessionId, - senderKey = senderKey, + senderKey = sessionInitiatorSenderKey, algorithm = roomKeyContent.algorithm ?: "", - userId = event.senderId ?: "", - deviceId = fromDevice ?: "", + userId = event.senderId.orEmpty(), + deviceId = fromDevice?.deviceId.orEmpty(), 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) + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index) } } @@ -296,7 +340,7 @@ internal class MXMegolmDecryption( .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() - onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) + onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId) } } 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 38edbb7430..99f8bc69e0 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,28 +17,36 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( private val olmDevice: MXOlmDevice, + @UserId private val myUserId: String, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val matrixConfiguration: MatrixConfiguration, - private val eventsManager: Lazy + private val eventsManager: Lazy, + private val unrequestedForwardManager: UnRequestedForwardManager, + private val mxCryptoConfig: MXCryptoConfig, + private val clock: Clock, ) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - olmDevice, - outgoingKeyRequestManager, - cryptoStore, - matrixConfiguration, - eventsManager + olmDevice = olmDevice, + myUserId = myUserId, + outgoingKeyRequestManager = outgoingKeyRequestManager, + cryptoStore = cryptoStore, + liveEventManager = eventsManager, + unrequestedForwardManager = unrequestedForwardManager, + cryptoConfig = mxCryptoConfig, + clock = clock, ) } } 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 771b5f9a62..0b7af9f4d7 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 @@ -31,6 +31,8 @@ import org.matrix.android.sdk.api.session.events.model.Content 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.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder import org.matrix.android.sdk.internal.crypto.MXOlmDevice @@ -92,7 +94,18 @@ internal class MXMegolmEncryption( ): Content { val ts = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") - val devices = getDevicesInRoom(userIds) + + /** + * When using in-room messages and the room has encryption enabled, + * clients should ensure that encryption does not hinder the verification. + * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s + * unverified devices receive the keys necessary to decrypt the messages, + * even if they would normally not be given the keys to decrypt messages in the room. + */ + val shouldSendToUnverified = isVerificationEvent(eventType, eventContent) + + val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified) + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") val outboundSession = ensureOutboundSession(devices.allowedDevices) @@ -107,6 +120,11 @@ internal class MXMegolmEncryption( } } + private fun isVerificationEvent(eventType: String, eventContent: Content) = + EventType.isVerificationEvent(eventType) || + (eventType == EventType.MESSAGE && + eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST) + private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { // offload to computation thread cryptoCoroutineScope.launch(coroutineDispatchers.computation) { @@ -162,7 +180,8 @@ internal class MXMegolmEncryption( forwardingCurve25519KeyChain = emptyList(), keysClaimed = keysClaimedMap, exportFormat = false, - sharedHistory = sharedHistory + sharedHistory = sharedHistory, + trusted = true ) defaultKeysBackupService.maybeBackupKeys() @@ -415,15 +434,17 @@ internal class MXMegolmEncryption( * This method must be called in getDecryptingThreadHandler() thread. * * @param userIds the user ids whose devices must be checked. + * @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if + * such devices are blocked in crypto settings */ - private suspend fun getDevicesInRoom(userIds: List): DeviceInRoomInfo { + private suspend fun getDevicesInRoom(userIds: List, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via // an m.new_device. val keys = deviceListManager.downloadKeys(userIds, false) val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || - cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + cryptoStore.getBlockUnverifiedDevices(roomId) val devicesInRoom = DeviceInRoomInfo() val unknownDevices = MXUsersDevicesMap() @@ -443,7 +464,7 @@ internal class MXMegolmEncryption( continue } - if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) { devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) continue } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt new file mode 100644 index 0000000000..9235cd2abf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt @@ -0,0 +1,150 @@ +/* + * 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.algorithms.megolm + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import timber.log.Timber +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.math.abs + +private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 + +@SessionScope +internal class UnRequestedForwardManager @Inject constructor( + private val deviceListManager: DeviceListManager, +) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val scope = CoroutineScope(SupervisorJob() + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + // For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups? + private val forwardedKeysPerRoom = mutableMapOf>>() + + data class InviteInfo( + val roomId: String, + val fromMxId: String, + val timestamp: Long + ) + + data class ForwardInfo( + val event: Event, + val timestamp: Long + ) + + // roomId, local timestamp of invite + private val recentInvites = mutableListOf() + + fun close() { + try { + scope.cancel("User Terminate") + } catch (failure: Throwable) { + Timber.w(failure, "Failed to shutDown UnrequestedForwardManager") + } + } + + fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) { + Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp") + scope.launch { + sequencer.post { + if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) { + recentInvites.add( + InviteInfo( + roomId, + fromUserId, + localTimeStamp + ) + ) + } + } + } + } + + fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) { + Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp") + scope.launch { + sequencer.post { + val claimSenderId = event.senderId.orEmpty() + val senderKey = event.getSenderKey() + // we might want to download keys, as this user might not be known yet, cache is ok + val ownerMxId = + tryOrNull { + deviceListManager.downloadKeys(listOf(claimSenderId), false) + .map[claimSenderId] + ?.values + ?.firstOrNull { it.identityKey() == senderKey } + ?.userId + } + // Not sure what to do if the device has been deleted? I can't proove the mxid + if (ownerMxId == null || claimSenderId != ownerMxId) { + Timber.w("Mismatch senderId between event and olm owner") + return@post + } + + forwardedKeysPerRoom + .getOrPut(roomId) { mutableMapOf() } + .getOrPut(ownerMxId) { mutableListOf() } + .add(ForwardInfo(event, localTimeStamp)) + } + } + } + + fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List) -> Unit) { + scope.launch { + sequencer.post { + // Prune outdated invites + recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS } + val cleanUpEvents = mutableListOf>() + forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) -> + senderIdToForwardMap.forEach { (senderId, eventList) -> + // is there a matching invite in a valid timewindow? + val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId } + if (matchingInvite != null) { + Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}") + + eventList.filter { + abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS + }.map { + it.event + }.takeIf { it.isNotEmpty() }?.let { + Timber.w("Re-processing forwarded_room_key_event that was not requested after invite") + scope.launch { + handleForwards.invoke(it) + } + } + cleanUpEvents.add(roomId to senderId) + } + } + } + + cleanUpEvents.forEach { roomIdToSenderPair -> + forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear() + } + } + } + } +} 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 d405bdce27..f4796155c6 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 @@ -60,7 +60,7 @@ import javax.inject.Inject @SessionScope internal class DefaultCrossSigningService @Inject constructor( - @UserId private val userId: String, + @UserId private val myUserId: String, @SessionId private val sessionId: String, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, @@ -127,7 +127,7 @@ internal class DefaultCrossSigningService @Inject constructor( } // Recover local trust in case private key are there? - setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified()) } } catch (e: Throwable) { // Mmm this kind of a big issue @@ -167,9 +167,13 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onSuccess(data: InitializeCrossSigningTask.Result) { - val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + val crossSigningInfo = MXCrossSigningInfo( + myUserId, + listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo), + true + ) cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } @@ -266,7 +270,7 @@ internal class DefaultCrossSigningService @Inject constructor( uskKeyPrivateKey: String?, sskPrivateKey: String? ): UserTrustResult { - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) var masterKeyIsTrusted = false var userKeyIsTrusted = false @@ -330,7 +334,7 @@ internal class DefaultCrossSigningService @Inject constructor( val checkSelfTrust = checkSelfTrust() if (checkSelfTrust.isVerified()) { cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) } return checkSelfTrust } @@ -351,7 +355,7 @@ internal class DefaultCrossSigningService @Inject constructor( * . */ override fun isUserTrusted(otherUserId: String): Boolean { - return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true } override fun isCrossSigningVerified(): Boolean { @@ -363,7 +367,7 @@ internal class DefaultCrossSigningService @Inject constructor( */ override fun checkUserTrust(otherUserId: String): UserTrustResult { Timber.v("## CrossSigning checkUserTrust for $otherUserId") - if (otherUserId == userId) { + if (otherUserId == myUserId) { return checkSelfTrust() } // I trust a user if I trust his master key @@ -371,16 +375,14 @@ internal class DefaultCrossSigningService @Inject constructor( // TODO what if the master key is signed by a device key that i have verified // First let's get my user key - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) - - checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - return UserTrustResult.Success + return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) } fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { val myUserKey = myCrossSigningInfo?.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) if (!myCrossSigningInfo.isTrusted()) { return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) @@ -391,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { @@ -417,9 +419,9 @@ internal class DefaultCrossSigningService @Inject constructor( // Special case when it's me, // I have to check that MSK -> USK -> SSK // and that MSK is trusted (i know the private key, or is signed by a trusted device) - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId)) + return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId)) } fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { @@ -429,7 +431,7 @@ internal class DefaultCrossSigningService @Inject constructor( // val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) val myMasterKey = myCrossSigningInfo?.masterKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) // Is the master key trusted // 1) check if I know the private key @@ -453,7 +455,7 @@ internal class DefaultCrossSigningService @Inject constructor( olmPkSigning?.releaseSigning() } else { // Maybe it's signed by a locally trusted device? - myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> val potentialDeviceId = key.removePrefix("ed25519:") val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { @@ -475,14 +477,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val myUserKey = myCrossSigningInfo.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK") return UserTrustResult.KeyNotSigned(myUserKey) } @@ -498,14 +500,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val mySSKey = myCrossSigningInfo.selfSigningKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK") return UserTrustResult.KeyNotSigned(mySSKey) } @@ -555,14 +557,14 @@ internal class DefaultCrossSigningService @Inject constructor( override fun trustUser(otherUserId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - Mark user $userId as trusted ") + Timber.d("## CrossSigning - Mark user $otherUserId as trusted ") // We should have this user keys val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() if (otherMasterKeys == null) { callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) return@launch @@ -586,16 +588,22 @@ internal class DefaultCrossSigningService @Inject constructor( } cryptoStore.setUserKeysAsTrusted(otherUserId, true) - // TODO update local copy with new signature directly here? kind of local echo of trust? - Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK") val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature)) .build() uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { this.executionThread = TaskThread.CRYPTO this.callback = callback }.executeBy(taskExecutor) + + // Local echo for device cross trust, to avoid having to wait for a notification of key change + cryptoStore.getUserDeviceList(otherUserId)?.forEach { device -> + val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } } } @@ -604,20 +612,20 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.markMyMasterKeyAsLocallyTrusted(true) checkSelfTrust() // re-verify all trusts - onUsersDeviceUpdate(listOf(userId)) + onUsersDeviceUpdate(listOf(myUserId)) } } override fun trustDevice(deviceId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // This device should be yours - val device = cryptoStore.getUserDevice(userId, deviceId) + val device = cryptoStore.getUserDevice(myUserId, deviceId) if (device == null) { callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("CrossSigning is not setup for this account")) return@launch @@ -639,7 +647,7 @@ internal class DefaultCrossSigningService @Inject constructor( } val toUpload = device.copy( signatures = mapOf( - userId + myUserId to mapOf( "ed25519:$ssPubKey" to newSignature @@ -661,8 +669,8 @@ internal class DefaultCrossSigningService @Inject constructor( val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) - val myKeys = getUserCrossSigningKeys(userId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + val myKeys = getUserCrossSigningKeys(myUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -717,7 +725,7 @@ internal class DefaultCrossSigningService @Inject constructor( fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() - myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -805,7 +813,7 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) // If it's me, recheck trust of all users and devices? val users = ArrayList() - if (otherUserId == userId && currentTrust != trusted) { + if (otherUserId == myUserId && currentTrust != trusted) { // notify key requester outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 6d845ec59e..fffc6707d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -161,6 +161,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses // i have all the new trusts, update DB trusts.forEach { val verified = it.value?.isVerified() == true + Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified") updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) } @@ -259,21 +260,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses cryptoRealm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() - ?.crossSigningKeys - ?.forEach { info -> - // optimization to avoid trigger updates when there is no change.. - if (info.trustLevelEntity?.isVerified() != verified) { - Timber.d("## CrossSigning - Trust change for $userId : $verified") - val level = info.trustLevelEntity - if (level == null) { - info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = verified - it.crossSignedVerified = verified + ?.let { userKeyInfo -> + userKeyInfo + .crossSigningKeys + .forEach { key -> + // optimization to avoid trigger updates when there is no change.. + if (key.trustLevelEntity?.isVerified() != verified) { + Timber.d("## CrossSigning - Trust change for $userId : $verified") + val level = key.trustLevelEntity + if (level == null) { + key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { + it.locallyVerified = verified + it.crossSignedVerified = verified + } + } else { + level.locallyVerified = verified + level.crossSignedVerified = verified + } + } } - } else { - level.locallyVerified = verified - level.crossSignedVerified = verified - } + if (verified) { + userKeyInfo.wasUserVerifiedOnce = true } } } @@ -299,8 +306,18 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true } + val resetTrust = listToCheck + .filter { userId -> + val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId) + crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true + } + return if (allTrustedUserIds.isEmpty()) { - RoomEncryptionTrustLevel.Default + if (resetTrust.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + RoomEncryptionTrustLevel.Warning + } } else { // If one of the verified user as an untrusted device -> warning // If all devices of all verified users are trusted -> green @@ -327,11 +344,15 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses if (hasWarning) { RoomEncryptionTrustLevel.Warning } else { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted + if (resetTrust.isEmpty()) { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } } else { - RoomEncryptionTrustLevel.Default + RoomEncryptionTrustLevel.Warning } } } @@ -344,7 +365,8 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } 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 8691c08779..e8700b7809 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 @@ -652,14 +652,7 @@ internal class DefaultKeysBackupService @Inject constructor( } 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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt index 2ce36aa209..15e8ba835b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt @@ -38,9 +38,6 @@ data class InboundGroupSessionData( @Json(name = "forwarding_curve25519_key_chain") var forwardingCurve25519KeyChain: List? = emptyList(), - /** Not yet used, will be in backup v2 - val untrusted?: Boolean = false */ - /** * Flag that indicates whether or not the current inboundSession will be shared to * invited users to decrypt past messages. @@ -48,4 +45,10 @@ data class InboundGroupSessionData( @Json(name = "shared_history") val sharedHistory: Boolean = false, + /** + * Flag indicating that this key is trusted. + */ + @Json(name = "trusted") + val trusted: Boolean? = null, + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt index 2772b34835..2c6a0a967a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt @@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper( keysClaimed = megolmSessionData.senderClaimedKeys, forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, sharedHistory = megolmSessionData.sharedHistory, + trusted = false ) return MXInboundMegolmSessionWrapper( 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 56eba25249..21e3342365 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 @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig 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 @@ -120,11 +121,26 @@ internal interface IMXCryptoStore { fun getRoomsListBlacklistUnverifiedDevices(): List /** - * Updates the rooms ids list in which the messages are not encrypted for the unverified devices. + * A live status regarding sharing keys for unverified devices in this room. * - * @param roomIds the room ids list + * @return Live status */ - fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData + + /** + * Tell if unverified devices should be blacklisted when sending keys. + * + * @return true if should not send keys to unverified devices + */ + fun getBlockUnverifiedDevices(roomId: String): Boolean + + /** + * Define if encryption keys should be sent to unverified devices in this room. + * + * @param roomId the roomId + * @param block if true will not send keys to unverified devices + */ + fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) /** * Get the current keys backup version. @@ -516,6 +532,9 @@ internal interface IMXCryptoStore { fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getLiveCrossSigningPrivateKeys(): LiveData> + fun getGlobalCryptoConfig(): GlobalCryptoConfig + fun getLiveGlobalCryptoConfig(): LiveData + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? 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 3b8fa4cacd..e97cf437c6 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 @@ -29,6 +29,7 @@ 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.GlobalCryptoConfig 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 @@ -445,6 +446,38 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getGlobalCryptoConfig(): GlobalCryptoConfig { + return doWithRealm(realmConfiguration) { realm -> + realm.where().findFirst() + ?.let { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } ?: GlobalCryptoConfig(false, false, false) + } + } + + override fun getLiveGlobalCryptoConfig(): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) + } + } + override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") doRealmTransaction(realmConfiguration) { realm -> @@ -1053,25 +1086,6 @@ internal class RealmCryptoStore @Inject constructor( } ?: false } - override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) { - doRealmTransaction(realmConfiguration) { - // Reset all - it.where() - .findAll() - .forEach { room -> - room.blacklistUnverifiedDevices = false - } - - // Enable those in the list - it.where() - .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray()) - .findAll() - .forEach { room -> - room.blacklistUnverifiedDevices = true - } - } - } - override fun getRoomsListBlacklistUnverifiedDevices(): List { return doWithRealm(realmConfiguration) { it.where() @@ -1083,6 +1097,37 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + }, + { + it.blacklistUnverifiedDevices + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: false + } + } + + override fun getBlockUnverifiedDevices(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() + ?.blacklistUnverifiedDevices ?: false + } + } + + override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId) + ?.blacklistUnverifiedDevices = block + } + } + override fun getDeviceTrackingStatuses(): Map { return doWithRealm(realmConfiguration) { it.where() @@ -1611,7 +1656,8 @@ internal class RealmCryptoStore @Inject constructor( userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } 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 c36d572da6..de2b74308d 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 @@ -34,6 +34,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo 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.crypto.store.db.migration.MigrateCryptoTo017 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -48,7 +50,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 17L, + schemaVersion = 19L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -75,5 +77,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 16) MigrateCryptoTo016(realm).perform() if (oldVersion < 17) MigrateCryptoTo017(realm).perform() + if (oldVersion < 18) MigrateCryptoTo018(realm).perform() + if (oldVersion < 19) MigrateCryptoTo019(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt new file mode 100644 index 0000000000..3bedf58ca2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt @@ -0,0 +1,52 @@ +/* + * 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.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import timber.log.Timber + +/** + * This migration is adding support for trusted flags on megolm sessions. + * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to + * mark existing keys as safe. + * This migration can take long depending on the account + */ +internal class MigrateCryptoTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) { + + private val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java) + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("OlmInboundGroupSessionEntity") + ?.transform { dynamicObject -> + try { + dynamicObject.getString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON)?.let { oldData -> + moshiAdapter.fromJson(oldData)?.let { dataToMigrate -> + dataToMigrate.copy(trusted = true).let { + dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(it)) + } + } + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to migrate megolm session") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt new file mode 100644 index 0000000000..9d2eb60a60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt @@ -0,0 +1,59 @@ +/* + * 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 io.realm.DynamicRealmObject +import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * This migration is adding support for trusted flags on megolm sessions. + * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to + * mark existing keys as safe. + * This migration can take long depending on the account + */ +internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 18) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CrossSigningInfoEntity") + ?.addField(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, Boolean::class.java) + ?.transform { dynamicObject -> + + val knowKeys = dynamicObject.getList(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`) + val msk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.MASTER.value) + } + val ssk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.SELF_SIGNING.value) + } + val isTrusted = isDynamicKeyInfoTrusted(msk?.get(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) && + isDynamicKeyInfoTrusted(ssk?.get(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) + + dynamicObject.setBoolean(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, isTrusted) + } + } + + private fun isDynamicKeyInfoTrusted(keyInfo: DynamicRealmObject?): Boolean { + if (keyInfo == null) return false + return !keyInfo.isNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && + !keyInfo.isNull(TrustLevelEntityFields.LOCALLY_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt index 5aba9bb9ba..033b7662c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class CrossSigningInfoEntity( @PrimaryKey var userId: String? = null, + var wasUserVerifiedOnce: Boolean = false, var crossSigningKeys: RealmList = RealmList() ) : RealmObject() { 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 a4b4cd0761..f93da74507 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 @@ -82,7 +82,8 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(), + isSafe = true ) } else { null 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 247b42307a..d74445b867 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 @@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 36L, + schemaVersion = 37L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -111,5 +112,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform() + if (oldVersion < 37) MigrateSessionTo037(realm).perform() } } 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 0a6d4bf833..193710f962 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 @@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) // Save decryption result, to not decrypt every time we enter the thread list eventEntity.setDecryptionResult(result) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt new file mode 100644 index 0000000000..09cb5985f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.database.mapper + +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import javax.inject.Inject + +internal class LocalRoomSummaryMapper @Inject constructor( + private val roomSummaryMapper: RoomSummaryMapper, +) { + + fun map(localRoomSummaryEntity: LocalRoomSummaryEntity): LocalRoomSummary { + return LocalRoomSummary( + roomId = localRoomSummaryEntity.roomId, + roomSummary = localRoomSummaryEntity.roomSummaryEntity?.let { roomSummaryMapper.map(it) }, + createRoomParams = localRoomSummaryEntity.createRoomParams, + replacementRoomId = localRoomSummaryEntity.replacementRoomId, + creationState = localRoomSummaryEntity.creationState + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt new file mode 100644 index 0000000000..cdb0b6c682 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.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.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo037(realm: DynamicRealm) : RealmMigrator(realm, 37) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LocalRoomSummaryEntity") + ?.addField(LocalRoomSummaryEntityFields.REPLACEMENT_ROOM_ID, String::class.java) + ?.addField(LocalRoomSummaryEntityFields.STATE_STR, String::class.java) + ?.transform { obj -> + obj.set(LocalRoomSummaryEntityFields.STATE_STR, LocalRoomCreationState.NOT_CREATED.name) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 8b5a211fba..ee5c3d90c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -87,7 +87,8 @@ internal open class EventEntity( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt index fd8331e986..a978e3719d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt @@ -18,15 +18,24 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.toJSONString internal open class LocalRoomSummaryEntity( @PrimaryKey var roomId: String = "", var roomSummaryEntity: RoomSummaryEntity? = null, - private var createRoomParamsStr: String? = null + var replacementRoomId: String? = null, ) : RealmObject() { + private var stateStr: String = LocalRoomCreationState.NOT_CREATED.name + var creationState: LocalRoomCreationState + get() = LocalRoomCreationState.valueOf(stateStr) + set(value) { + stateStr = value.name + } + + private var createRoomParamsStr: String? = null var createRoomParams: CreateRoomParams? get() { return CreateRoomParams.fromJson(createRoomParamsStr) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt index 527350bedc..44730eb75d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt @@ -22,10 +22,6 @@ import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields -internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { - val query = realm.where() - if (roomId != null) { - query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) - } - return query +internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where().equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index b180c06e2c..170814d3f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -33,6 +33,11 @@ internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: Strin .equalTo(ReadReceiptEntityFields.USER_ID, userId) } +internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) +} + internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { this.primaryKey = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt new file mode 100644 index 0000000000..6eb4d5b104 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt @@ -0,0 +1,81 @@ +/* + * 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.network + +import android.content.Context +import android.os.Build +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import javax.inject.Inject + +class ComputeUserAgentUseCase @Inject constructor( + private val context: Context, +) { + + /** + * Create an user agent with the application version. + * Ex: Element/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + * + * @param flavorDescription the flavor description + */ + fun execute(flavorDescription: String): String { + val appPackageName = context.applicationContext.packageName + val pm = context.packageManager + + val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() } + ?.takeIf { + it.matches("\\A\\p{ASCII}*\\z".toRegex()) + } + ?: run { + // Use appPackageName instead of appName if appName is null or contains any non-ASCII character + appPackageName + } + val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION + + val deviceManufacturer = Build.MANUFACTURER + val deviceModel = Build.MODEL + val androidVersion = Build.VERSION.RELEASE + val deviceBuildId = Build.DISPLAY + val matrixSdkVersion = BuildConfig.SDK_VERSION + + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(deviceManufacturer) + append(" ") + append(deviceModel) + append("; ") + append("Android ") + append(androidVersion) + append("; ") + append(deviceBuildId) + append("; ") + append("Flavour ") + append(flavorDescription) + append("; ") + append("MatrixAndroidSdk2 ") + append(matrixSdkVersion) + append(")") + } + } + + companion object { + const val FALLBACK_APP_VERSION = "0.0.0" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt index 28d96dfce7..4e83261277 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -16,73 +16,20 @@ package org.matrix.android.sdk.internal.network -import android.content.Context -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.di.MatrixScope -import timber.log.Timber import javax.inject.Inject @MatrixScope internal class UserAgentHolder @Inject constructor( - private val context: Context, - matrixConfiguration: MatrixConfiguration + matrixConfiguration: MatrixConfiguration, + computeUserAgentUseCase: ComputeUserAgentUseCase, ) { var userAgent: String = "" private set init { - setApplicationFlavor(matrixConfiguration.applicationFlavor) - } - - /** - * Create an user agent with the application version. - * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) - * - * @param flavorDescription the flavor description - */ - private fun setApplicationFlavor(flavorDescription: String) { - var appName = "" - var appVersion = "" - - try { - val appPackageName = context.applicationContext.packageName - val pm = context.packageManager - val appInfo = pm.getApplicationInfo(appPackageName, 0) - appName = pm.getApplicationLabel(appInfo).toString() - - val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) - appVersion = pkgInfo.versionName ?: "" - - // Use appPackageName instead of appName if appName contains any non-ASCII character - if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { - appName = appPackageName - } - } catch (e: Exception) { - Timber.e(e, "## initUserAgent() : failed") - } - - val systemUserAgent = System.getProperty("http.agent") - - // cannot retrieve the application version - if (appName.isEmpty() || appVersion.isEmpty()) { - if (null == systemUserAgent) { - userAgent = "Java" + System.getProperty("java.version") - } - return - } - - // if there is no user agent or cannot parse it - if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { - userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") - } else { - // update - userAgent = appName + "/" + appVersion + " " + - systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + - "; Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")" - } + userAgent = computeUserAgentUseCase.execute(matrixConfiguration.applicationFlavor) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt index b2ab9879df..a93ff42c9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -38,6 +38,7 @@ internal class QueryStringValueProcessor @Inject constructor( is ContentQueryStringValue -> when (queryStringValue) { is QueryStringValue.Equals -> equalTo(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) is QueryStringValue.Contains -> contains(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) + is QueryStringValue.NotContains -> not().process(field, QueryStringValue.Contains(queryStringValue.string, queryStringValue.case)) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index abea2d34cd..262c111b73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService @@ -82,6 +83,14 @@ internal class DefaultRoom( return roomSummaryDataSource.getRoomSummary(roomId) } + override fun getLocalRoomSummaryLive(): LiveData> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + + override fun localRoomSummary(): LocalRoomSummary? { + return roomSummaryDataSource.getLocalRoomSummary(roomId) + } + override fun asSpace(): Space? { if (roomSummary()?.roomType != RoomType.SPACE) return null return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 989bcaee44..6d72b8ef20 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -29,10 +29,12 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary 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.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount @@ -106,6 +108,10 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummaryLive(roomId) } + override fun getLocalRoomSummaryLive(roomId: String): LiveData> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + override fun getRoomSummaries( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder @@ -173,7 +179,10 @@ internal class DefaultRoomService @Inject constructor( } override suspend fun onRoomDisplayed(roomId: String) { - updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + // Do not add local rooms to the recent rooms list as they should not be known by the server + if (!RoomLocalEcho.isLocalEchoId(roomId)) { + updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + } } override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt index 02538a5cc3..2245eb8513 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt @@ -17,38 +17,23 @@ package org.matrix.android.sdk.internal.session.room.create import com.zhuinden.monarchy.Monarchy -import io.realm.kotlin.where import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +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.tombstone.RoomTombstoneContent -import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource 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.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -56,94 +41,100 @@ import javax.inject.Inject * Create a room on the server from a local room. * The configuration of the local room will be use to configure the new room. * The potential local room members will also be invited to this new room. - * - * A local tombstone event will be created to indicate that the local room has been replacing by the new one. */ internal interface CreateRoomFromLocalRoomTask : Task { data class Params(val localRoomId: String) } internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor( - @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, - private val stateEventDataSource: StateEventDataSource, - private val clock: Clock, + private val roomSummaryDataSource: RoomSummaryDataSource, ) : CreateRoomFromLocalRoomTask { private val realmConfiguration get() = monarchy.realmConfiguration override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String { - val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel() - ?.replacementRoomId + val localRoomSummary = roomSummaryDataSource.getLocalRoomSummary(params.localRoomId) + ?: error("## CreateRoomFromLocalRoomTask - Cannot retrieve LocalRoomSummary with roomId ${params.localRoomId}") - if (replacementRoomId != null) { - return replacementRoomId + // If a room has already been created for the given local room, return the existing roomId + if (localRoomSummary.replacementRoomId != null) { + return localRoomSummary.replacementRoomId } - var createRoomParams: CreateRoomParams? = null - var isEncrypted = false - monarchy.doWithRealm { realm -> - realm.where() - .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId) - .findFirst() - ?.let { - createRoomParams = it.createRoomParams - isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse() - } + if (localRoomSummary.createRoomParams != null && localRoomSummary.roomSummary != null) { + return createRoom(params.localRoomId, localRoomSummary.roomSummary, localRoomSummary.createRoomParams) + } else { + error("## CreateRoomFromLocalRoomTask - Invalid LocalRoomSummary: $localRoomSummary") } - val roomId = createRoomTask.execute(createRoomParams!!) + } + /** + * Create a room on the server for the given local room. + * + * @param localRoomId the local room identifier. + * @param localRoomSummary the RoomSummary of the local room. + * @param createRoomParams the CreateRoomParams object which was used to configure the local room. + * + * @return the identifier of the created room. + */ + private suspend fun createRoom(localRoomId: String, localRoomSummary: RoomSummary, createRoomParams: CreateRoomParams): String { + updateCreationState(localRoomId, LocalRoomCreationState.CREATING) + val replacementRoomId = runCatching { + createRoomTask.execute(createRoomParams) + }.fold( + { it }, + { + updateCreationState(localRoomId, LocalRoomCreationState.FAILURE) + throw it + } + ) + updateReplacementRoomId(localRoomId, replacementRoomId) + waitForRoomEvents(replacementRoomId, localRoomSummary) + updateCreationState(localRoomId, LocalRoomCreationState.CREATED) + return replacementRoomId + } + + /** + * Wait for all the room events before triggering the created state. + * + * @param replacementRoomId the identifier of the created room + * @param localRoomSummary the RoomSummary of the local room. + */ + private suspend fun waitForRoomEvents(replacementRoomId: String, localRoomSummary: RoomSummary) { try { - // Wait for all the room events before triggering the replacement room awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> realm.where(RoomSummaryEntity::class.java) - .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) - .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0) + .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId) + .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, localRoomSummary.invitedMembersCount) } awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY) } - if (isEncrypted) { + if (localRoomSummary.isEncrypted) { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) } } } catch (exception: TimeoutCancellationException) { - throw CreateRoomFailure.CreatedWithTimeout(roomId) + updateCreationState(localRoomSummary.roomId, LocalRoomCreationState.FAILURE) + throw CreateRoomFailure.CreatedWithTimeout(replacementRoomId) } + } - createTombstoneEvent(params, roomId) - return roomId + private fun updateCreationState(roomId: String, creationState: LocalRoomCreationState) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, roomId).findFirst()?.creationState = creationState + } } - /** - * Create a Tombstone event to indicate that the local room has been replaced by a new one. - */ - private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) { - val now = clock.epochMillis() - val event = Event( - type = EventType.STATE_ROOM_TOMBSTONE, - senderId = userId, - originServerTs = now, - stateKey = "", - eventId = UUID.randomUUID().toString(), - content = RoomTombstoneContent( - replacementRoomId = roomId - ).toContent() - ) - monarchy.awaitTransaction { realm -> - val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) - if (event.stateKey != null && event.type != null && event.eventId != null) { - CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply { - eventId = event.eventId - root = eventEntity - } - } + private fun updateReplacementRoomId(localRoomId: String, replacementRoomId: String) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, localRoomId).findFirst()?.replacementRoomId = replacementRoomId } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt index 49951d2da0..a60c7e6a27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt @@ -22,12 +22,15 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereInRoom import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params @@ -50,6 +53,12 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor( if (RoomLocalEcho.isLocalEchoId(roomId)) { monarchy.awaitTransaction { realm -> Timber.i("## DeleteLocalRoomTask - delete local room id $roomId") + ReadReceiptsSummaryEntity.whereInRoom(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptsSummaryEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + ReadReceiptEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll() ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") } ?.deleteAllFromRealm() 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 bac810f424..edd74c2ce0 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 @@ -225,7 +225,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (e: MXCryptoError) { if (e is MXCryptoError.Base) { 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 deleted file mode 100644 index 55363a7251..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ /dev/null @@ -1,235 +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.session.room.send.queue - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.auth.data.sessionId -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.isLimitExceededError -import org.matrix.android.sdk.api.failure.isTokenError -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import timber.log.Timber -import java.io.IOException -import java.util.Timer -import java.util.TimerTask -import java.util.concurrent.LinkedBlockingQueue -import javax.inject.Inject -import kotlin.concurrent.schedule - -/** - * A simple ever running thread unique for that session responsible of sending events in order. - * Each send is retried 3 times, if there is no network (e.g if cannot ping homeserver) it will wait and - * periodically test reachability before resume (does not count as a retry) - * - * If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted - */ -@Deprecated("You should know use EventSenderProcessorCoroutine instead") -@SessionScope -internal class EventSenderProcessorThread @Inject constructor( - private val cryptoService: CryptoService, - private val sessionParams: SessionParams, - private val queuedTaskFactory: QueuedTaskFactory, - private val taskExecutor: TaskExecutor, - private val memento: QueueMemento -) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor { - - private fun markAsManaged(task: QueuedTask) { - memento.track(task) - } - - private fun markAsFinished(task: QueuedTask) { - memento.unTrack(task) - } - - override fun onSessionStarted(session: Session) { - start() - } - - override fun onSessionStopped(session: Session) { - interrupt() - } - - override fun start() { - super.start() - // We should check for sending events not handled because app was killed - // But we should be careful of only took those that was submitted to us, because if it's - // for example it's a media event it is handled by some worker and he will handle it - // This is a bit fragile :/ - // also some events cannot be retried manually by users, e.g reactions - // they were previously relying on workers to do the work :/ and was expected to always finally succeed - // Also some echos are not to be resent like redaction echos (fake event created for aggregation) - - tryOrNull { - taskExecutor.executorScope.launch { - Timber.d("## Send relaunched pending events on restart") - memento.restoreTasks(this@EventSenderProcessorThread) - } - } - } - - // API - override fun postEvent(event: Event): Cancelable { - return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false) - } - - override fun postEvent(event: Event, encrypt: Boolean): Cancelable { - val task = queuedTaskFactory.createSendTask(event, encrypt) - return postTask(task) - } - - override fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable { - return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason) - } - - override fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable { - val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason) - return postTask(task) - } - - override fun postTask(task: QueuedTask): Cancelable { - // non blocking add to queue - sendingQueue.add(task) - markAsManaged(task) - return task - } - - override fun cancel(eventId: String, roomId: String) { - (currentTask as? SendEventQueuedTask) - ?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId } - ?.cancel() - } - - companion object { - private const val RETRY_WAIT_TIME_MS = 10_000L - } - - private var currentTask: QueuedTask? = null - - private var sendingQueue = LinkedBlockingQueue() - - private var networkAvailableLock = Object() - private var canReachServer = true - private var retryNoNetworkTask: TimerTask? = null - - override fun run() { - Timber.v("## SendThread started") - try { - while (!isInterrupted) { - Timber.v("## SendThread wait for task to process") - val task = sendingQueue.take() - .also { currentTask = it } - Timber.v("## SendThread Found task to process $task") - - if (task.isCancelled()) { - Timber.v("## SendThread send cancelled for $task") - // we do not execute this one - continue - } - // we check for network connectivity - while (!canReachServer) { - Timber.v("## SendThread cannot reach server") - // schedule to retry - waitForNetwork() - // if thread as been killed meanwhile -// if (state == State.KILLING) break - } - Timber.v("## Server is Reachable") - // so network is available - - runBlocking { - retryLoop@ while (task.retryCount.get() < 3) { - try { - // SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER) - Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}") - task.execute() - // sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService)) - // SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER) - break@retryLoop - } catch (exception: Throwable) { - when { - exception is IOException || exception is Failure.NetworkConnection -> { - canReachServer = false - if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() - while (!canReachServer) { - Timber.v("## SendThread retryLoop cannot reach server") - // schedule to retry - waitForNetwork() - } - } - (exception.isLimitExceededError()) -> { - if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() - Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}") - // wait a bit - // Todo if its a quota exception can we get timout? - sleep(3_000) - continue@retryLoop - } - exception.isTokenError() -> { - Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt") - // we can exit the loop - task.onTaskFailed() - throw InterruptedException() - } - exception is CancellationException -> { - Timber.v("## SendThread task has been cancelled") - break@retryLoop - } - else -> { - Timber.v("## SendThread retryLoop Un-Retryable error, try next task") - // this task is in error, check next one? - task.onTaskFailed() - break@retryLoop - } - } - } - } - } - markAsFinished(task) - } - } catch (interruptionException: InterruptedException) { - // will be thrown is thread is interrupted while seeping - interrupt() - Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}") - } -// state = State.KILLED - // is this needed? - retryNoNetworkTask?.cancel() - Timber.w("## SendThread finished") - } - - private fun waitForNetwork() { - retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { - synchronized(networkAvailableLock) { - canReachServer = HomeServerAvailabilityChecker(sessionParams).check().also { - Timber.v("## SendThread checkHostAvailable $it") - } - networkAvailableLock.notify() - } - } - synchronized(networkAvailableLock) { networkAvailableLock.wait() } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 19598aa3e5..2435afd607 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary 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.RoomType @@ -43,7 +44,9 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.LocalRoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.findByAlias @@ -57,6 +60,7 @@ import javax.inject.Inject internal class RoomSummaryDataSource @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, + private val localRoomSummaryMapper: LocalRoomSummaryMapper, private val queryStringValueProcessor: QueryStringValueProcessor, ) { @@ -95,6 +99,25 @@ internal class RoomSummaryDataSource @Inject constructor( ) } + fun getLocalRoomSummary(roomId: String): LocalRoomSummary? { + return monarchy + .fetchCopyMap({ + LocalRoomSummaryEntity.where(it, roomId).findFirst() + }, { entity, _ -> + localRoomSummaryMapper.map(entity) + }) + } + + fun getLocalRoomSummaryLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> LocalRoomSummaryEntity.where(realm, roomId) }, + { localRoomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + fun getRoomSummariesLive( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE @@ -272,6 +295,7 @@ internal class RoomSummaryDataSource @Inject constructor( val query = with(queryStringValueProcessor) { RoomSummaryEntity.where(realm) .process(RoomSummaryEntityFields.ROOM_ID, QueryStringValue.IsNotEmpty) + .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) .process(queryParams.displayName.toDisplayNameField(), queryParams.displayName) .process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) .process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) 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 7c662444e4..e0751865ad 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 @@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index d2f1b3202b..cd13b03017 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.LiveData import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.session.space.SpaceHierarchyData import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.di.UserId @@ -128,7 +128,7 @@ internal class DefaultSpaceService @Inject constructor( suggestedOnly: Boolean?, limit: Int?, from: String?, - knownStateList: List? + knownStateList: List? ): SpaceHierarchyData { val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from) val spaceRootResponse = spacesResponse.getRoot(spaceId) @@ -180,7 +180,7 @@ internal class DefaultSpaceService @Inject constructor( private fun List?.mapSpaceChildren( spaceId: String, spaceRootResponse: SpaceChildSummaryResponse?, - knownStateList: List?, + knownStateList: List?, ) = this?.filterIdIsNot(spaceId) ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList) .orEmpty() @@ -190,7 +190,7 @@ internal class DefaultSpaceService @Inject constructor( private fun List.toSpaceChildInfoList( spaceId: String, rootRoomResponse: SpaceChildSummaryResponse?, - knownStateList: List?, + knownStateList: List?, ) = flatMap { spaceChildSummary -> (rootRoomResponse?.childrenState ?: knownStateList) ?.filter { it.isChildOf(spaceChildSummary) } @@ -198,10 +198,14 @@ internal class DefaultSpaceService @Inject constructor( .orEmpty() } - private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD + private fun SpaceChildSummaryEvent.isChildOf(space: SpaceChildSummaryResponse): Boolean { + return stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD + } - private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel()?.let { content -> - createSpaceChildInfo(spaceId, summary, content) + private fun SpaceChildSummaryEvent.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse): SpaceChildInfo? { + return content.toModel()?.let { content -> + createSpaceChildInfo(spaceId, summary, content) + } } private fun createSpaceChildInfo( @@ -255,7 +259,7 @@ internal class DefaultSpaceService @Inject constructor( stateKey = QueryStringValue.IsEmpty ) val powerLevelsContent = powerLevelsEvent?.content?.toModel() - ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel") + ?: throw UnsupportedOperationException("Cannot add canonical child, missing power level") val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { throw UnsupportedOperationException("Cannot add canonical child, not enough power level") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt index e3f8977ac5..0419c5acf1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.space import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent /** * The fields are the same as those returned by /publicRooms (see spec), with the addition of: @@ -36,10 +36,11 @@ internal data class SpaceChildSummaryResponse( */ @Json(name = "room_type") val roomType: String? = null, - /** The m.space.child events of the room. For each event, only the following fields are included: - * type, state_key, content, room_id, sender, with the addition of origin_server_ts. + /** + * The m.space.child events of the room. For each event, only the following fields are included: + * type, state_key, content, sender, and of origin_server_ts. */ - @Json(name = "children_state") val childrenState: List? = null, + @Json(name = "children_state") val childrenState: List? = null, /** * Aliases of the room. May be empty. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index b6142b3a7a..b2fe12ebc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.sync.handler +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM 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.model.MXEventDecryptionResult @@ -42,17 +43,41 @@ internal class CryptoSyncHandler @Inject constructor( suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { val total = toDevice.events?.size ?: 0 - toDevice.events?.forEachIndexed { index, event -> - progressReporter?.reportProgress(index * 100F / total) - // Decrypt event if necessary - Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") - decryptToDeviceEvent(event, null) - if (event.getClearType() == EventType.MESSAGE && - event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { - Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") - } else { - verificationService.onToDeviceEvent(event) - cryptoService.onToDeviceEvent(event) + toDevice.events + ?.filter { isSupportedToDevice(it) } + ?.forEachIndexed { index, event -> + progressReporter?.reportProgress(index * 100F / total) + // Decrypt event if necessary + Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") + decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE && + event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { + Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") + } else { + verificationService.onToDeviceEvent(event) + cryptoService.onToDeviceEvent(event) + } + } + } + + private val unsupportedPlainToDeviceEventTypes = listOf( + EventType.ROOM_KEY, + EventType.FORWARDED_ROOM_KEY, + EventType.SEND_SECRET + ) + + private fun isSupportedToDevice(event: Event): Boolean { + val algorithm = event.content?.get("algorithm") as? String + val type = event.type.orEmpty() + return if (event.isEncrypted()) { + algorithm == MXCRYPTO_ALGORITHM_OLM + } else { + // some clear events are not allowed + type !in unsupportedPlainToDeviceEventTypes + }.also { + if (!it) { + Timber.tag(loggerTag.value) + .w("Ignoring unsupported to device event ${event.type} alg:${algorithm}") } } } @@ -91,7 +116,8 @@ internal class CryptoSyncHandler @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) return true } else { 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 bc91ca205d..a2f2251b70 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 @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomSync import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.createOrUpdate @@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor( private val timelineInput: TimelineInput, private val liveEventService: Lazy, private val clock: Clock, + private val unRequestedForwardManager: UnRequestedForwardManager, ) { sealed class HandlingStrategy { @@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor( } roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator) + unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis()) return roomEntity } @@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (e: MXCryptoError) { if (e is MXCryptoError.Base) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index b47b215655..d3f2a3f044 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError @@ -52,7 +53,6 @@ import javax.inject.Inject import kotlin.concurrent.schedule private const val RETRY_WAIT_TIME_MS = 10_000L -private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC) @@ -61,7 +61,8 @@ internal class SyncThread @Inject constructor( private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler, - private val lightweightSettingsStorage: DefaultLightweightSettingsStorage + private val lightweightSettingsStorage: DefaultLightweightSettingsStorage, + private val matrixConfiguration: MatrixConfiguration, ) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle @@ -181,7 +182,7 @@ internal class SyncThread @Inject constructor( val timeout = when { previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ afterPause -> 0L /* No timeout after a pause */ - else -> DEFAULT_LONG_POOL_TIMEOUT + else -> matrixConfiguration.syncConfig.longPollTimeout } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") val presence = lightweightSettingsStorage.getSyncPresenceStatus() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt new file mode 100644 index 0000000000..5b41ff6da0 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt @@ -0,0 +1,250 @@ +/* + * 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 io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.fail +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent +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.UnsignedDeviceInfo +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.toContent +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager + +class UnRequestedKeysManagerTest { + + private val aliceMxId = "alice@example.com" + private val bobMxId = "bob@example.com" + private val bobDeviceId = "MKRJDSLYGA" + + private val device1Id = "MGDAADVDMG" + + private val aliceFirstDevice = CryptoDeviceInfo( + deviceId = device1Id, + userId = aliceMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", + "ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", + ), + signatures = mapOf( + aliceMxId to mapOf( + "ed25519:$device1Id" + to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", + "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" + to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), + trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) + ) + + private val aBobDevice = CryptoDeviceInfo( + deviceId = bobDeviceId, + userId = bobMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", + "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", + ), + signatures = mapOf( + bobMxId to mapOf( + "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") + ) + + @Test + fun `test process key request if invite received`() { + val fakeDeviceListManager = mockk { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + 1_000 + ) + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId2" + ), + 1_000 + ) + // for now no reason to accept + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { + fail("There should be no key to process") + } + } + + // ACT + // suppose an invite is received but from another user + val inviteTime = 1_000L + unrequestedForwardManager.onInviteReceived(roomId, "@jhon:example.com", inviteTime) + + // we shouldn't process the requests! +// runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + fail("There should be no key to process") + } +// } + + // ACT + // suppose an invite is received from correct user + + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + it.size shouldBe 2 + } + } + } + + @Test + fun `test invite before keys`() { + val fakeDeviceListManager = mockk { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, 1_000) + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + 1_000 + ) + + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { + it.size shouldBe 1 + } + } + } + + @Test + fun `test validity window`() { + val fakeDeviceListManager = mockk { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + val timeOfKeyReception = 1_000L + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + timeOfKeyReception + ) + + val currentTimeWindow = 10 * 60_000 + + // simulate very late invite + val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000 + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) + + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + fail("There should be no key to process") + } + } + } + + private fun createFakeSuccessfullyDecryptedForwardToDevice( + sentBy: CryptoDeviceInfo, + dest: CryptoDeviceInfo, + sessionInitiator: CryptoDeviceInfo, + algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, + roomId: String = "!zzgDlIhbWOevcdFBXr:example.com", + megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw" + ): Event { + return Event( + type = EventType.ENCRYPTED, + eventId = "!fake", + senderId = sentBy.userId, + content = OlmEventContent( + ciphertext = mapOf( + dest.identityKey()!! to mapOf( + "type" to 0, + "body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+" + ) + ), + senderKey = sentBy.identityKey() + ).toContent(), + + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to ForwardedRoomKeyContent( + algorithm = algorithm, + roomId = roomId, + senderKey = sessionInitiator.identityKey(), + sessionId = megolmSessionId, + sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..." + ).toContent() + ), + senderKey = sentBy.identityKey() + ) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt new file mode 100644 index 0000000000..9ed6f28d7e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * 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.network + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.BuildConfig +import java.lang.Exception + +private const val A_PACKAGE_NAME = "org.matrix.sdk" +private const val AN_APP_NAME = "Element" +private const val A_NON_ASCII_APP_NAME = "Élement" +private const val AN_APP_VERSION = "1.5.1" +private const val A_FLAVOUR = "GooglePlay" + +class ComputeUserAgentUseCaseTest { + + private val context = mockk() + private val packageManager = mockk() + private val applicationInfo = mockk() + private val packageInfo = mockk() + + private val computeUserAgentUseCase = ComputeUserAgentUseCase(context) + + @Before + fun setUp() { + every { context.applicationContext } returns context + every { context.packageName } returns A_PACKAGE_NAME + every { context.packageManager } returns packageManager + every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo + every { packageManager.getPackageInfo(any(), any()) } returns packageInfo + } + + @Test + fun `given a non-null app name and app version when computing user agent then returns expected user agent`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(null) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a non-ascii app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(A_NON_ASCII_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app version when computing user agent then returns user agent with a fallback app version`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(null) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, ComputeUserAgentUseCase.FALLBACK_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + private fun constructExpectedUserAgent(appName: String, appVersion: String): String { + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(Build.MANUFACTURER) + append(" ") + append(Build.MODEL) + append("; ") + append("Android ") + append(Build.VERSION.RELEASE) + append("; ") + append(Build.DISPLAY) + append("; ") + append("Flavour ") + append(A_FLAVOUR) + append("; ") + append("MatrixAndroidSdk2 ") + append(BuildConfig.SDK_VERSION) + append(")") + } + } + + private fun givenAppName(deviceName: String?) { + if (deviceName == null) { + every { packageManager.getApplicationLabel(any()) } throws Exception("Cannot retrieve application name") + } else if (!deviceName.matches("\\A\\p{ASCII}*\\z".toRegex())) { + every { packageManager.getApplicationLabel(any()) } returns A_PACKAGE_NAME + } else { + every { packageManager.getApplicationLabel(any()) } returns deviceName + } + } + + private fun givenAppVersion(appVersion: String?) { + packageInfo.versionName = appVersion + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt index d3732363b5..9e34280437 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt @@ -22,21 +22,22 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyOrder import io.realm.kotlin.where import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull import org.junit.After import org.junit.Before import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity @@ -44,29 +45,24 @@ import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.getOrCreate -import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.test.fakes.FakeMonarchy -import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource +import org.matrix.android.sdk.test.fakes.FakeRoomSummaryDataSource private const val A_LOCAL_ROOM_ID = "local.a-local-room-id" private const val AN_EXISTING_ROOM_ID = "an-existing-room-id" private const val A_ROOM_ID = "a-room-id" -private const val MY_USER_ID = "my-user-id" @ExperimentalCoroutinesApi internal class DefaultCreateRoomFromLocalRoomTaskTest { private val fakeMonarchy = FakeMonarchy() - private val clock = DefaultClock() private val createRoomTask = mockk() - private val fakeStateEventDataSource = FakeStateEventDataSource() + private val fakeRoomSummaryDataSource = FakeRoomSummaryDataSource() private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask( - userId = MY_USER_ID, monarchy = fakeMonarchy.instance, createRoomTask = createRoomTask, - stateEventDataSource = fakeStateEventDataSource.instance, - clock = clock + roomSummaryDataSource = fakeRoomSummaryDataSource.instance, ) @Before @@ -91,13 +87,12 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { @Test fun `given a local room id when execute then the existing room id is kept`() = runTest { // Given - givenATombstoneEvent( - Event( - roomId = A_LOCAL_ROOM_ID, - type = EventType.STATE_ROOM_TOMBSTONE, - stateKey = "", - content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent() - ) + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aCreationState = LocalRoomCreationState.CREATED, aReplacementRoomId = AN_EXISTING_ROOM_ID) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity( + aCreateRoomParams = aCreateRoomParams, + aCreationState = LocalRoomCreationState.CREATED, + aReplacementRoomId = AN_EXISTING_ROOM_ID ) // When @@ -105,20 +100,18 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(AN_EXISTING_ROOM_ID) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) result shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED } @Test fun `given a local room id when execute then it is correctly executed`() = runTest { // Given - val aCreateRoomParams = mockk() - val aLocalRoomSummaryEntity = mockk { - every { roomSummaryEntity } returns mockk(relaxed = true) - every { createRoomParams } returns aCreateRoomParams - } - givenATombstoneEvent(null) - givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity) + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID @@ -127,32 +120,84 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(null) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) // CreateRoomTask has been called with the initial CreateRoomParams coVerify { createRoomTask.execute(aCreateRoomParams) } // The resulting roomId matches the roomId returned by the createRoomTask result shouldBeEqualTo A_ROOM_ID - // A tombstone state event has been created - coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATED + } + // The local room summary has been updated with the created room id + verify { aLocalRoomSummaryEntity.replacementRoomId = A_ROOM_ID } + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo A_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED + } + + @Test + fun `given a local room id when execute with an exception then the creation state is correctly updated`() = runTest { + // Given + val aCreateRoomParams = mockk(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + + coEvery { createRoomTask.execute(any()) }.throws(mockk()) + + // When + val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID) + tryOrNull { defaultCreateRoomFromLocalRoomTask.execute(params) } + + // Then + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) + // CreateRoomTask has been called with the initial CreateRoomParams + coVerify { createRoomTask.execute(aCreateRoomParams) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.FAILURE + } + // The local room summary has been updated with the created room id + aLocalRoomSummaryEntity.replacementRoomId.shouldBeNull() + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.FAILURE } - private fun givenATombstoneEvent(event: Event?) { - fakeStateEventDataSource.givenGetStateEventReturns(event) + private fun givenALocalRoomSummary( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummary { + val aLocalRoomSummary = LocalRoomSummary( + roomId = A_LOCAL_ROOM_ID, + roomSummary = mockk(relaxed = true), + createRoomParams = aCreateRoomParams, + creationState = aCreationState, + replacementRoomId = aReplacementRoomId, + ) + fakeRoomSummaryDataSource.givenGetLocalRoomSummaryReturns(A_LOCAL_ROOM_ID, aLocalRoomSummary) + return aLocalRoomSummary } - private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) { + private fun givenALocalRoomSummaryEntity( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummaryEntity { + val aLocalRoomSummaryEntity = spyk(LocalRoomSummaryEntity( + roomId = A_LOCAL_ROOM_ID, + roomSummaryEntity = mockk(relaxed = true), + replacementRoomId = aReplacementRoomId, + ).apply { + createRoomParams = aCreateRoomParams + creationState = aCreationState + }) every { fakeMonarchy.fakeRealm.instance .where() .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID) .findFirst() - } returns localRoomSummaryEntity - } - - private fun verifyTombstoneEvent(expectedRoomId: String?) { - fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel() - ?.replacementRoomId shouldBeEqualTo expectedRoomId + } returns aLocalRoomSummaryEntity + return aLocalRoomSummaryEntity } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 2d501f12af..93999458c6 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -47,6 +47,11 @@ internal class FakeMonarchy { } coAnswers { firstArg().doWithRealm(fakeRealm.instance) } + coEvery { + instance.runTransactionSync(any()) + } coAnswers { + firstArg().execute(fakeRealm.instance) + } every { instance.realmConfiguration } returns mockk() } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt new file mode 100644 index 0000000000..c7b70a3ad5 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource + +internal class FakeRoomSummaryDataSource { + + val instance: RoomSummaryDataSource = mockk() + + fun givenGetLocalRoomSummaryReturns(roomId: String?, localRoomSummary: LocalRoomSummary?) { + every { instance.getLocalRoomSummary(roomId = roomId ?: any()) } returns localRoomSummary + } + + fun verifyGetLocalRoomSummary(roomId: String) { + verify { instance.getLocalRoomSummary(roomId) } + } +} diff --git a/settings.gradle b/settings.gradle index e5b5511b94..d35476f769 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,16 @@ include ':vector-app' include ':vector' include ':vector-config' -include ':matrix-sdk-android' + include ':library:core-utils' include ':library:ui-strings' include ':library:ui-styles' -include ':library:jsonviewer' include ':library:attachment-viewer' -include ':library:diff-match-patch' include ':library:multipicker' + +include ':library:external:jsonviewer' +include ':library:external:diff-match-patch' +include ':library:external:dialpad' + +include ':matrix-sdk-android' include ':matrix-sdk-android-flow' diff --git a/tools/danger/dangerfile.js b/tools/danger/dangerfile.js index 87f979c7f5..d2e0d423f3 100644 --- a/tools/danger/dangerfile.js +++ b/tools/danger/dangerfile.js @@ -107,3 +107,10 @@ if (hasPngs) { if (github.requested_reviewers.users.length == 0 && !pr.draft) { warn("Please add a reviewer to your PR.") } + +// Check that translations have not been modified by developers +if (user != "RiotTranslateBot") { + if (editedFiles.some(file => file.endsWith("strings.xml") && !file.endsWith("values/strings.xml"))) { + fail("Some translation files have been edited. Only user `RiotTranslateBot` (i.e. translations coming from Weblate) is allowed to do that.\nPlease read more about translations management [in the doc](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#internationalisation).") + } +} diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index 41465a442f..c00bd10371 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -54,6 +54,7 @@ "grimacing-face", "face-exhaling", "lying-face", + "shaking-face", "relieved-face", "pensive-face", "sleepy-face", @@ -104,7 +105,7 @@ "tired-face", "yawning-face", "face-with-steam-from-nose", - "pouting-face", + "enraged-face", "angry-face", "face-with-symbols-on-mouth", "smiling-face-with-horns", @@ -131,7 +132,6 @@ "seenoevil-monkey", "hearnoevil-monkey", "speaknoevil-monkey", - "kiss-mark", "love-letter", "heart-with-arrow", "heart-with-ribbon", @@ -146,14 +146,18 @@ "heart-on-fire", "mending-heart", "red-heart", + "pink-heart", "orange-heart", "yellow-heart", "green-heart", "blue-heart", + "light-blue-heart", "purple-heart", "brown-heart", "black-heart", + "grey-heart", "white-heart", + "kiss-mark", "hundred-points", "anger-symbol", "collision", @@ -161,7 +165,6 @@ "sweat-droplets", "dashing-away", "hole", - "bomb", "speech-balloon", "eye-in-speech-bubble", "left-speech-bubble", @@ -183,6 +186,8 @@ "leftwards-hand", "palm-down-hand", "palm-up-hand", + "leftwards-pushing-hand", + "rightwards-pushing-hand", "ok-hand", "pinched-fingers", "pinching-hand", @@ -561,6 +566,8 @@ "tiger", "leopard", "horse-face", + "moose", + "donkey", "horse", "unicorn", "zebra", @@ -623,6 +630,9 @@ "flamingo", "peacock", "parrot", + "wing", + "black-bird", + "goose", "frog", "crocodile", "turtle", @@ -643,6 +653,7 @@ "octopus", "spiral-shell", "coral", + "jellyfish", "snail", "butterfly", "bug", @@ -670,6 +681,7 @@ "sunflower", "blossom", "tulip", + "hyacinth", "seedling", "potted-plant", "evergreen-tree", @@ -684,7 +696,8 @@ "fallen-leaf", "leaf-fluttering-in-wind", "empty-nest", - "nest-with-eggs" + "nest-with-eggs", + "mushroom" ] }, { @@ -722,10 +735,11 @@ "broccoli", "garlic", "onion", - "mushroom", "peanuts", "beans", "chestnut", + "ginger-root", + "pea-pod", "bread", "croissant", "baguette-bread", @@ -1110,11 +1124,10 @@ "bullseye", "yoyo", "kite", + "water-pistol", "pool-8-ball", "crystal-ball", "magic-wand", - "nazar-amulet", - "hamsa", "video-game", "joystick", "slot-machine", @@ -1165,6 +1178,7 @@ "shorts", "bikini", "womans-clothes", + "folding-hand-fan", "purse", "handbag", "clutch-bag", @@ -1179,6 +1193,7 @@ "womans-sandal", "ballet-shoes", "womans-boot", + "hair-pick", "crown", "womans-hat", "top-hat", @@ -1217,6 +1232,8 @@ "banjo", "drum", "long-drum", + "maracas", + "flute", "mobile-phone", "mobile-phone-with-arrow", "telephone", @@ -1336,7 +1353,7 @@ "hammer-and-wrench", "dagger", "crossed-swords", - "water-pistol", + "bomb", "boomerang", "bow-and-arrow", "shield", @@ -1397,6 +1414,8 @@ "coffin", "headstone", "funeral-urn", + "nazar-amulet", + "hamsa", "moai", "placard", "identification-card" @@ -1465,6 +1484,7 @@ "peace-symbol", "menorah", "dotted-sixpointed-star", + "khanda", "aries", "taurus", "gemini", @@ -1500,6 +1520,7 @@ "dim-button", "bright-button", "antenna-bars", + "wireless", "vibration-mode", "mobile-phone-off", "female-sign", @@ -2050,7 +2071,7 @@ ] }, "melting-face": { - "a": "⊛ Melting Face", + "a": "Melting Face", "b": "1FAE0", "j": [ "disappear", @@ -2345,7 +2366,7 @@ ] }, "face-with-open-eyes-and-hand-over-mouth": { - "a": "⊛ Face with Open Eyes and Hand over Mouth", + "a": "Face with Open Eyes and Hand over Mouth", "b": "1FAE2", "j": [ "amazement", @@ -2360,7 +2381,7 @@ ] }, "face-with-peeking-eye": { - "a": "⊛ Face with Peeking Eye", + "a": "Face with Peeking Eye", "b": "1FAE3", "j": [ "captivated", @@ -2368,7 +2389,8 @@ "stare", "scared", "frightening", - "embarrassing" + "embarrassing", + "shy" ] }, "shushing-face": { @@ -2393,10 +2415,10 @@ ] }, "saluting-face": { - "a": "⊛ Saluting Face", + "a": "Saluting Face", "b": "1FAE1", "j": [ - "ok", + "OK", "salute", "sunny", "troops", @@ -2469,7 +2491,7 @@ ] }, "dotted-line-face": { - "a": "⊛ Dotted Line Face", + "a": "Dotted Line Face", "b": "1FAE5", "j": [ "depressed", @@ -2569,6 +2591,17 @@ "pinocchio" ] }, + "shaking-face": { + "a": "⊛ Shaking Face", + "b": "1FAE8", + "j": [ + "earthquake", + "face", + "shaking", + "shock", + "vibrate" + ] + }, "relieved-face": { "a": "Relieved Face", "b": "1F60C", @@ -2598,6 +2631,7 @@ "b": "1F62A", "j": [ "face", + "good night", "sleep", "tired", "rest", @@ -2617,11 +2651,13 @@ "b": "1F634", "j": [ "face", + "good night", "sleep", - "zzz", + "ZZZ", "tired", "sleepy", - "night" + "night", + "zzz" ] }, "face-with-medical-mask": { @@ -2851,9 +2887,10 @@ "a": "Face with Monocle", "b": "1F9D0", "j": [ + "face", + "monocle", "stuffy", - "wealthy", - "face" + "wealthy" ] }, "confused-face": { @@ -2871,7 +2908,7 @@ ] }, "face-with-diagonal-mouth": { - "a": "⊛ Face with Diagonal Mouth", + "a": "Face with Diagonal Mouth", "b": "1FAE4", "j": [ "disappointed", @@ -2980,7 +3017,7 @@ ] }, "face-holding-back-tears": { - "a": "⊛ Face Holding Back Tears", + "a": "Face Holding Back Tears", "b": "1F979", "j": [ "angry", @@ -3191,16 +3228,18 @@ "pride" ] }, - "pouting-face": { - "a": "Pouting Face", + "enraged-face": { + "a": "Enraged Face", "b": "1F621", "j": [ "angry", + "enraged", "face", "mad", "pouting", "rage", "red", + "pouting_face", "hate", "despise" ] @@ -3578,19 +3617,6 @@ "omg" ] }, - "kiss-mark": { - "a": "Kiss Mark", - "b": "1F48B", - "j": [ - "kiss", - "lips", - "face", - "love", - "like", - "affection", - "valentines" - ] - }, "love-letter": { "a": "Love Letter", "b": "1F48C", @@ -3764,6 +3790,17 @@ "valentines" ] }, + "pink-heart": { + "a": "⊛ Pink Heart", + "b": "1FA77", + "j": [ + "cute", + "heart", + "like", + "love", + "pink" + ] + }, "orange-heart": { "a": "Orange Heart", "b": "1F9E1", @@ -3808,6 +3845,17 @@ "valentines" ] }, + "light-blue-heart": { + "a": "⊛ Light Blue Heart", + "b": "1FA75", + "j": [ + "cyan", + "heart", + "light blue", + "light blue heart", + "teal" + ] + }, "purple-heart": { "a": "Purple Heart", "b": "1F49C", @@ -3837,6 +3885,17 @@ "wicked" ] }, + "grey-heart": { + "a": "⊛ Grey Heart", + "b": "1FA76", + "j": [ + "gray", + "grey heart", + "heart", + "silver", + "slate" + ] + }, "white-heart": { "a": "White Heart", "b": "1F90D", @@ -3846,6 +3905,19 @@ "pure" ] }, + "kiss-mark": { + "a": "Kiss Mark", + "b": "1F48B", + "j": [ + "kiss", + "lips", + "face", + "love", + "like", + "affection", + "valentines" + ] + }, "hundred-points": { "a": "Hundred Points", "b": "1F4AF", @@ -3930,17 +4002,6 @@ "embarrassing" ] }, - "bomb": { - "a": "Bomb", - "b": "1F4A3", - "j": [ - "comic", - "boom", - "explode", - "explosion", - "terrorism" - ] - }, "speech-balloon": { "a": "Speech Balloon", "b": "1F4AC", @@ -3960,8 +4021,10 @@ "a": "Eye in Speech Bubble", "b": "1F441-FE0F-200D-1F5E8-FE0F", "j": [ + "balloon", + "bubble", "eye", - "speech bubble", + "speech", "witness", "info" ] @@ -3970,6 +4033,8 @@ "a": "Left Speech Bubble", "b": "1F5E8", "j": [ + "balloon", + "bubble", "dialog", "speech", "words", @@ -4010,7 +4075,9 @@ "b": "1F4A4", "j": [ "comic", + "good night", "sleep", + "ZZZ", "sleepy", "tired", "dream" @@ -4080,7 +4147,7 @@ ] }, "rightwards-hand": { - "a": "⊛ Rightwards Hand", + "a": "Rightwards Hand", "b": "1FAF1", "j": [ "hand", @@ -4091,7 +4158,7 @@ ] }, "leftwards-hand": { - "a": "⊛ Leftwards Hand", + "a": "Leftwards Hand", "b": "1FAF2", "j": [ "hand", @@ -4102,7 +4169,7 @@ ] }, "palm-down-hand": { - "a": "⊛ Palm Down Hand", + "a": "Palm Down Hand", "b": "1FAF3", "j": [ "dismiss", @@ -4112,7 +4179,7 @@ ] }, "palm-up-hand": { - "a": "⊛ Palm Up Hand", + "a": "Palm Up Hand", "b": "1FAF4", "j": [ "beckon", @@ -4123,6 +4190,32 @@ "demand" ] }, + "leftwards-pushing-hand": { + "a": "⊛ Leftwards Pushing Hand", + "b": "1FAF7", + "j": [ + "high five", + "leftward", + "leftwards pushing hand", + "push", + "refuse", + "stop", + "wait" + ] + }, + "rightwards-pushing-hand": { + "a": "⊛ Rightwards Pushing Hand", + "b": "1FAF8", + "j": [ + "high five", + "push", + "refuse", + "rightward", + "rightwards pushing hand", + "stop", + "wait" + ] + }, "ok-hand": { "a": "Ok Hand", "b": "1F44C", @@ -4186,7 +4279,7 @@ ] }, "hand-with-index-finger-and-thumb-crossed": { - "a": "⊛ Hand with Index Finger and Thumb Crossed", + "a": "Hand with Index Finger and Thumb Crossed", "b": "1FAF0", "j": [ "expensive", @@ -4228,6 +4321,8 @@ "j": [ "call", "hand", + "hang loose", + "Shaka", "hands", "gesture", "shaka" @@ -4313,7 +4408,7 @@ ] }, "index-pointing-at-the-viewer": { - "a": "⊛ Index Pointing at the Viewer", + "a": "Index Pointing at the Viewer", "b": "1FAF5", "j": [ "point", @@ -4429,7 +4524,7 @@ ] }, "heart-hands": { - "a": "⊛ Heart Hands", + "a": "Heart Hands", "b": "1FAF6", "j": [ "love", @@ -4686,7 +4781,7 @@ ] }, "biting-lip": { - "a": "⊛ Biting Lip", + "a": "Biting Lip", "b": "1FAE6", "j": [ "anxious", @@ -6088,7 +6183,7 @@ ] }, "person-with-crown": { - "a": "⊛ Person with Crown", + "a": "Person with Crown", "b": "1FAC5", "j": [ "monarch", @@ -6262,7 +6357,7 @@ ] }, "pregnant-man": { - "a": "⊛ Pregnant Man", + "a": "Pregnant Man", "b": "1FAC3", "j": [ "belly", @@ -6273,7 +6368,7 @@ ] }, "pregnant-person": { - "a": "⊛ Pregnant Person", + "a": "Pregnant Person", "b": "1FAC4", "j": [ "belly", @@ -6669,7 +6764,7 @@ ] }, "troll": { - "a": "⊛ Troll", + "a": "Troll", "b": "1F9CC", "j": [ "fairy tale", @@ -7633,6 +7728,7 @@ "a": "Person in Bed", "b": "1F6CC", "j": [ + "good night", "hotel", "sleep", "bed", @@ -8514,6 +8610,30 @@ "nature" ] }, + "moose": { + "a": "⊛ Moose", + "b": "1FACE", + "j": [ + "animal", + "antlers", + "elk", + "mammal", + "moose" + ] + }, + "donkey": { + "a": "⊛ Donkey", + "b": "1FACF", + "j": [ + "animal", + "ass", + "burro", + "donkey", + "mammal", + "mule", + "stubborn" + ] + }, "horse": { "a": "Horse", "b": "1F40E", @@ -9180,6 +9300,40 @@ "nature" ] }, + "wing": { + "a": "⊛ Wing", + "b": "1FABD", + "j": [ + "angelic", + "aviation", + "bird", + "flying", + "mythology", + "wing" + ] + }, + "black-bird": { + "a": "⊛ Black Bird", + "b": "1F426-200D-2B1B", + "j": [ + "bird", + "black", + "crow", + "raven", + "rook" + ] + }, + "goose": { + "a": "⊛ Goose", + "b": "1FABF", + "j": [ + "bird", + "fowl", + "goose", + "honk", + "silly" + ] + }, "frog": { "a": "Frog", "b": "1F438", @@ -9410,7 +9564,7 @@ ] }, "coral": { - "a": "⊛ Coral", + "a": "Coral", "b": "1FAB8", "j": [ "ocean", @@ -9418,6 +9572,19 @@ "sea" ] }, + "jellyfish": { + "a": "⊛ Jellyfish", + "b": "1FABC", + "j": [ + "burn", + "invertebrate", + "jelly", + "jellyfish", + "marine", + "ouch", + "stinger" + ] + }, "snail": { "a": "Snail", "b": "1F40C", @@ -9621,7 +9788,7 @@ ] }, "lotus": { - "a": "⊛ Lotus", + "a": "Lotus", "b": "1FAB7", "j": [ "Buddhism", @@ -9662,7 +9829,8 @@ "flower", "wilted", "plant", - "nature" + "nature", + "rose" ] }, "hibiscus": { @@ -9709,6 +9877,18 @@ "spring" ] }, + "hyacinth": { + "a": "⊛ Hyacinth", + "b": "1FABB", + "j": [ + "bluebonnet", + "flower", + "hyacinth", + "lavender", + "lupine", + "snapdragon" + ] + }, "seedling": { "a": "Seedling", "b": "1F331", @@ -9873,7 +10053,7 @@ ] }, "empty-nest": { - "a": "⊛ Empty Nest", + "a": "Empty Nest", "b": "1FAB9", "j": [ "nesting", @@ -9881,13 +10061,22 @@ ] }, "nest-with-eggs": { - "a": "⊛ Nest with Eggs", + "a": "Nest with Eggs", "b": "1FABA", "j": [ "nesting", "bird" ] }, + "mushroom": { + "a": "Mushroom", + "b": "1F344", + "j": [ + "toadstool", + "plant", + "vegetable" + ] + }, "grapes": { "a": "Grapes", "b": "1F347", @@ -10199,15 +10388,6 @@ "spice" ] }, - "mushroom": { - "a": "Mushroom", - "b": "1F344", - "j": [ - "toadstool", - "plant", - "vegetable" - ] - }, "peanuts": { "a": "Peanuts", "b": "1F95C", @@ -10219,7 +10399,7 @@ ] }, "beans": { - "a": "⊛ Beans", + "a": "Beans", "b": "1FAD8", "j": [ "food", @@ -10236,6 +10416,28 @@ "squirrel" ] }, + "ginger-root": { + "a": "⊛ Ginger Root", + "b": "1FADA", + "j": [ + "beer", + "ginger root", + "root", + "spice" + ] + }, + "pea-pod": { + "a": "⊛ Pea Pod", + "b": "1FADB", + "j": [ + "beans", + "edamame", + "legume", + "pea", + "pod", + "vegetable" + ] + }, "bread": { "a": "Bread", "b": "1F35E", @@ -11080,7 +11282,8 @@ "tea", "caffeine", "latte", - "espresso" + "espresso", + "mug" ] }, "teapot": { @@ -11255,7 +11458,7 @@ ] }, "pouring-liquid": { - "a": "⊛ Pouring Liquid", + "a": "Pouring Liquid", "b": "1FAD7", "j": [ "drink", @@ -11384,7 +11587,7 @@ ] }, "jar": { - "a": "⊛ Jar", + "a": "Jar", "b": "1FAD9", "j": [ "condiment", @@ -12075,7 +12278,7 @@ ] }, "playground-slide": { - "a": "⊛ Playground Slide", + "a": "Playground Slide", "b": "1F6DD", "j": [ "amusement park", @@ -12606,7 +12809,7 @@ ] }, "wheel": { - "a": "⊛ Wheel", + "a": "Wheel", "b": "1F6DE", "j": [ "circle", @@ -12688,7 +12891,7 @@ ] }, "ring-buoy": { - "a": "⊛ Ring Buoy", + "a": "Ring Buoy", "b": "1F6DF", "j": [ "float", @@ -14683,6 +14886,20 @@ "wind" ] }, + "water-pistol": { + "a": "Water Pistol", + "b": "1F52B", + "j": [ + "gun", + "handgun", + "pistol", + "revolver", + "tool", + "water", + "weapon", + "violence" + ] + }, "pool-8-ball": { "a": "Pool 8 Ball", "b": "1F3B1", @@ -14726,30 +14943,6 @@ "power" ] }, - "nazar-amulet": { - "a": "Nazar Amulet", - "b": "1F9FF", - "j": [ - "bead", - "charm", - "evil-eye", - "nazar", - "talisman" - ] - }, - "hamsa": { - "a": "⊛ Hamsa", - "b": "1FAAC", - "j": [ - "amulet", - "Fatima", - "hand", - "Mary", - "Miriam", - "protection", - "religion" - ] - }, "video-game": { "a": "Video Game", "b": "1F3AE", @@ -14831,7 +15024,7 @@ ] }, "mirror-ball": { - "a": "⊛ Mirror Ball", + "a": "Mirror Ball", "b": "1FAA9", "j": [ "dance", @@ -15252,6 +15445,19 @@ "female" ] }, + "folding-hand-fan": { + "a": "⊛ Folding Hand Fan", + "b": "1FAAD", + "j": [ + "cooling", + "dance", + "fan", + "flutter", + "folding hand fan", + "hot", + "shy" + ] + }, "purse": { "a": "Purse", "b": "1F45B", @@ -15426,6 +15632,16 @@ "fashion" ] }, + "hair-pick": { + "a": "⊛ Hair Pick", + "b": "1FAAE", + "j": [ + "Afro", + "comb", + "hair", + "pick" + ] + }, "crown": { "a": "Crown", "b": "1F451", @@ -15864,6 +16080,30 @@ "music" ] }, + "maracas": { + "a": "⊛ Maracas", + "b": "1FA87", + "j": [ + "instrument", + "maracas", + "music", + "percussion", + "rattle", + "shake" + ] + }, + "flute": { + "a": "⊛ Flute", + "b": "1FA88", + "j": [ + "fife", + "flute", + "music", + "pipe", + "recorder", + "woodwind" + ] + }, "mobile-phone": { "a": "Mobile Phone", "b": "1F4F1", @@ -15941,7 +16181,7 @@ ] }, "low-battery": { - "a": "⊛ Low Battery", + "a": "Low Battery", "b": "1FAAB", "j": [ "electronic", @@ -16054,7 +16294,7 @@ "a": "Optical Disk", "b": "1F4BF", "j": [ - "cd", + "CD", "computer", "disk", "optical", @@ -16068,9 +16308,10 @@ "a": "Dvd", "b": "1F4C0", "j": [ - "blu-ray", + "Blu-ray", "computer", "disk", + "DVD", "optical", "cd", "disc" @@ -17258,18 +17499,15 @@ "weapon" ] }, - "water-pistol": { - "a": "Water Pistol", - "b": "1F52B", + "bomb": { + "a": "Bomb", + "b": "1F4A3", "j": [ - "gun", - "handgun", - "pistol", - "revolver", - "tool", - "water", - "weapon", - "violence" + "comic", + "boom", + "explode", + "explosion", + "terrorism" ] }, "boomerang": { @@ -17584,7 +17822,7 @@ ] }, "crutch": { - "a": "⊛ Crutch", + "a": "Crutch", "b": "1FA7C", "j": [ "cane", @@ -17607,7 +17845,7 @@ ] }, "xray": { - "a": "⊛ X-Ray", + "a": "X-Ray", "b": "1FA7B", "j": [ "bones", @@ -17814,7 +18052,7 @@ ] }, "bubbles": { - "a": "⊛ Bubbles", + "a": "Bubbles", "b": "1FAE7", "j": [ "burp", @@ -17917,6 +18155,30 @@ "rip" ] }, + "nazar-amulet": { + "a": "Nazar Amulet", + "b": "1F9FF", + "j": [ + "bead", + "charm", + "evil-eye", + "nazar", + "talisman" + ] + }, + "hamsa": { + "a": "Hamsa", + "b": "1FAAC", + "j": [ + "amulet", + "Fatima", + "hand", + "Mary", + "Miriam", + "protection", + "religion" + ] + }, "moai": { "a": "Moai", "b": "1F5FF", @@ -17940,7 +18202,7 @@ ] }, "identification-card": { - "a": "⊛ Identification Card", + "a": "Identification Card", "b": "1FAAA", "j": [ "credentials", @@ -17954,7 +18216,7 @@ "a": "Atm Sign", "b": "1F3E7", "j": [ - "atm", + "ATM", "ATM sign", "automated", "bank", @@ -18006,13 +18268,15 @@ "a": "Men’S Room", "b": "1F6B9", "j": [ + "bathroom", "lavatory", "man", "men’s room", "restroom", - "wc", - "men_s_room", "toilet", + "WC", + "men_s_room", + "wc", "blue-square", "gender", "male" @@ -18022,15 +18286,16 @@ "a": "Women’S Room", "b": "1F6BA", "j": [ + "bathroom", "lavatory", "restroom", - "wc", + "toilet", + "WC", "woman", "women’s room", "women_s_room", "purple-square", "female", - "toilet", "loo", "gender" ] @@ -18039,10 +18304,11 @@ "a": "Restroom", "b": "1F6BB", "j": [ + "bathroom", "lavatory", + "toilet", "WC", "blue-square", - "toilet", "refresh", "wc", "gender" @@ -18062,12 +18328,13 @@ "a": "Water Closet", "b": "1F6BE", "j": [ + "bathroom", "closet", "lavatory", "restroom", - "water", - "wc", "toilet", + "water", + "WC", "blue-square" ] }, @@ -18504,8 +18771,7 @@ "b": "1F519", "j": [ "arrow", - "back", - "BACK arrow", + "BACK", "words", "return" ] @@ -18515,8 +18781,7 @@ "b": "1F51A", "j": [ "arrow", - "end", - "END arrow", + "END", "words" ] }, @@ -18526,8 +18791,8 @@ "j": [ "arrow", "mark", - "on", - "ON! arrow", + "ON", + "ON!", "words" ] }, @@ -18536,8 +18801,7 @@ "b": "1F51C", "j": [ "arrow", - "soon", - "SOON arrow", + "SOON", "words" ] }, @@ -18546,8 +18810,7 @@ "b": "1F51D", "j": [ "arrow", - "top", - "TOP arrow", + "TOP", "up", "words", "blue-square" @@ -18689,6 +18952,15 @@ "hexagram" ] }, + "khanda": { + "a": "⊛ Khanda", + "b": "1FAAF", + "j": [ + "khanda", + "religion", + "Sikh" + ] + }, "aries": { "a": "Aries", "b": "2648", @@ -18966,7 +19238,6 @@ "j": [ "arrow", "button", - "red", "blue-square", "triangle", "direction", @@ -18993,7 +19264,6 @@ "arrow", "button", "down", - "red", "blue-square", "direction", "bottom" @@ -19103,6 +19373,16 @@ "bars" ] }, + "wireless": { + "a": "⊛ Wireless", + "b": "1F6DC", + "j": [ + "computer", + "internet", + "network", + "wireless" + ] + }, "vibration-mode": { "a": "Vibration Mode", "b": "1F4F3", @@ -19213,7 +19493,7 @@ ] }, "heavy-equals-sign": { - "a": "⊛ Heavy Equals Sign", + "a": "Heavy Equals Sign", "b": "1F7F0", "j": [ "equality", @@ -19592,7 +19872,7 @@ "a": "Copyright", "b": "00A9", "j": [ - "c", + "C", "ip", "license", "circle", @@ -19604,7 +19884,7 @@ "a": "Registered", "b": "00AE", "j": [ - "r", + "R", "alphabet", "circle" ] @@ -19614,7 +19894,7 @@ "b": "2122", "j": [ "mark", - "tm", + "TM", "trademark", "brand", "law", @@ -19812,7 +20092,7 @@ "a": "A Button (Blood Type)", "b": "1F170", "j": [ - "a", + "A", "A button (blood type)", "blood type", "a_button", @@ -19825,7 +20105,7 @@ "a": "Ab Button (Blood Type)", "b": "1F18E", "j": [ - "ab", + "AB", "AB button (blood type)", "blood type", "ab_button", @@ -19837,7 +20117,7 @@ "a": "B Button (Blood Type)", "b": "1F171", "j": [ - "b", + "B", "B button (blood type)", "blood type", "b_button", @@ -19850,7 +20130,7 @@ "a": "Cl Button", "b": "1F191", "j": [ - "cl", + "CL", "CL button", "alphabet", "words", @@ -19861,7 +20141,7 @@ "a": "Cool Button", "b": "1F192", "j": [ - "cool", + "COOL", "COOL button", "words", "blue-square" @@ -19871,7 +20151,7 @@ "a": "Free Button", "b": "1F193", "j": [ - "free", + "FREE", "FREE button", "blue-square", "words" @@ -19891,7 +20171,7 @@ "a": "Id Button", "b": "1F194", "j": [ - "id", + "ID", "ID button", "identity", "purple-square", @@ -19904,7 +20184,7 @@ "j": [ "circle", "circled M", - "m", + "M", "alphabet", "blue-circle", "letter" @@ -19914,7 +20194,7 @@ "a": "New Button", "b": "1F195", "j": [ - "new", + "NEW", "NEW button", "blue-square", "words", @@ -19925,7 +20205,7 @@ "a": "Ng Button", "b": "1F196", "j": [ - "ng", + "NG", "NG button", "blue-square", "words", @@ -19938,7 +20218,7 @@ "b": "1F17E", "j": [ "blood type", - "o", + "O", "O button (blood type)", "o_button", "alphabet", @@ -19962,6 +20242,7 @@ "a": "P Button", "b": "1F17F", "j": [ + "P", "P button", "parking", "cars", @@ -19975,7 +20256,7 @@ "b": "1F198", "j": [ "help", - "sos", + "SOS", "SOS button", "red-square", "words", @@ -19988,7 +20269,8 @@ "b": "1F199", "j": [ "mark", - "up", + "UP", + "UP!", "UP! button", "blue-square", "above", @@ -20000,7 +20282,7 @@ "b": "1F19A", "j": [ "versus", - "vs", + "VS", "VS button", "words", "orange-square" diff --git a/tools/gradle/doctor.gradle b/tools/gradle/doctor.gradle new file mode 100644 index 0000000000..c77d2eb338 --- /dev/null +++ b/tools/gradle/doctor.gradle @@ -0,0 +1,92 @@ +// Default configuration copied from https://runningcode.github.io/gradle-doctor/configuration/ + +def isCiBuild = System.env.BUILDKITE == "true" || System.env.GITHUB_ACTIONS == "true" +println "Is CI build: $isCiBuild" + +doctor { + /** + * Throw an exception when multiple Gradle Daemons are running. + * + * Windows is not supported yet, see https://github.com/runningcode/gradle-doctor/issues/84 + */ + disallowMultipleDaemons = false + /** + * Show a message if the download speed is less than this many megabytes / sec. + */ + downloadSpeedWarningThreshold = 0.5f + /** + * The level at which to warn when a build spends more than this percent garbage collecting. + */ + GCWarningThreshold = 0.10f + /** + * The level at which to fail when a build spends more than this percent garbage collecting. + */ + GCFailThreshold = 0.9f + /** + * Print a warning to the console if we spend more than this amount of time with Dagger annotation processors. + */ + daggerThreshold = 5000 + /** + * By default, Gradle caches test results. This can be dangerous if tests rely on timestamps, dates, or other files + * which are not declared as inputs. + */ + enableTestCaching = true + /** + * By default, Gradle treats empty directories as inputs to compilation tasks. This can cause cache misses. + */ + failOnEmptyDirectories = true + /** + * Do not allow building all apps simultaneously. This is likely not what the user intended. + */ + allowBuildingAllAndroidAppsSimultaneously = false + /** + * Warn if using Android Jetifier. It slows down builds. + */ + warnWhenJetifierEnabled = true + /** + * Negative Avoidance Savings Threshold + * By default the Gradle Doctor will print out a warning when a task is slower to pull from the cache than to + * re-execute. There is some variance in the amount of time a task can take when several tasks are running + * concurrently. In order to account for this there is a threshold you can set. When the difference is above the + * threshold, a warning is displayed. + */ + negativeAvoidanceThreshold = 500 + /** + * Warn when not using parallel GC. Parallel GC is faster for build type tasks and is no longer the default in Java 9+. + */ + warnWhenNotUsingParallelGC = !isCiBuild + /** + * Throws an error when the `Delete` or `clean` task has dependencies. + * If a clean task depends on other tasks, clean can be reordered and made to run after the tasks that would produce + * output. This can lead to build failures or just strangeness with seemingly straightforward builds + * (e.g., gradle clean build). + * http://github.com/gradle/gradle/issues/2488 + */ + disallowCleanTaskDependencies = true + /** + * Warn if using the Kotlin Compiler Daemon Fallback. The fallback is incredibly slow and should be avoided. + * https://youtrack.jetbrains.com/issue/KT-48843 + */ + warnIfKotlinCompileDaemonFallback = true + + /** Configuration properties relating to JAVA_HOME */ + javaHome { + /** + * Ensure that we are using JAVA_HOME to build with this Gradle. + */ + ensureJavaHomeMatches = true + /** + * Ensure we have JAVA_HOME set. + */ + ensureJavaHomeIsSet = true + /** + * Fail on any `JAVA_HOME` issues. + */ + failOnError.set(!isCiBuild) + /** + * Extra message text, if any, to show with the Gradle Doctor message. This is useful if you have a wiki page or + * other instructions that you want to link for developers on your team if they encounter an issue. + */ + extraMessage.set("Here's an extra message to show.") + } +} diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index e754e5b9bd..c15522c48d 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -19,6 +19,9 @@ + + + @@ -86,6 +89,7 @@ + @@ -96,6 +100,7 @@ + @@ -113,6 +118,9 @@ + + + diff --git a/tools/validate_lfs.sh b/tools/validate_lfs.sh new file mode 100755 index 0000000000..ce121057b6 --- /dev/null +++ b/tools/validate_lfs.sh @@ -0,0 +1,29 @@ +#! /bin/bash + +# +# Copyright (c) 2022 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Based on https://cashapp.github.io/paparazzi/#git-lfs + +# Compare the output of `git ls-files ':(attr:filter=lfs)'` against `git lfs ls-files` +# If there's no diff we assume the files have been committed using git lfs +diff <(git ls-files ':(attr:filter=lfs)' | sort) <(git lfs ls-files -n | sort) >/dev/null + +ret=$? +if [[ $ret -ne 0 ]]; then + echo >&2 "Detected files committed without using Git LFS." + echo >&2 "Install git lfs (eg brew install git-lfs) and run 'git lfs install --local' within the root repository directory and re-commit your files." + exit 1 +fi diff --git a/towncrier.toml b/towncrier.toml index 6e78815f0f..003512e1c9 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] - version = "2.5.0" + version = "2.5.2" directory = "changelog.d" filename = "TCHAP_CHANGES.md" name = "Changes in Tchap" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 96e4791ac2..8436c20f9d 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -36,7 +36,7 @@ ext.versionMinor = 5 // 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 = 0 +ext.versionPatch = 2 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -197,7 +197,7 @@ android { // Known limitation: it does not modify the value in the BuildConfig.java generated file // See https://issuetracker.google.com/issues/171133218 output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode - print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n" + print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCode + "\n" output.outputFileName = output.outputFileName.replace("vector-app", "vector") } } @@ -301,6 +301,13 @@ android { // } } + // Tchap : no night mode +// sourceSets { +// nightly { +// java.srcDirs += "src/release/java" +// } +// } + // The 'store' dimension permits to deal with GooglePlay/Fdroid app // The 'target' dimension permits to specify which platform are used // The 'voip' flavor dimension permits to include/exclude jitsi at compilation time @@ -406,16 +413,171 @@ android { "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ] } + + buildFeatures { + viewBinding true + } +} + +// Tchap: Use custom configuration for Flipper library +configurations { + fdroidBtchapWithvoipWithoutpinningDebugImplementation + fdroidDevTchapWithvoipWithoutpinningDebugImplementation + fdroidTchapWithvoipWithoutpinningDebugImplementation + + gplayBtchapWithvoipWithoutpinningDebugImplementation + gplayDevTchapWithvoipWithoutpinningDebugImplementation + gplayTchapWithvoipWithoutpinningDebugImplementation + + + fdroidBtchapWithvoipWithpinningDebugImplementation + fdroidDevTchapWithvoipWithpinningDebugImplementation + fdroidTchapWithvoipWithpinningDebugImplementation + + gplayBtchapWithvoipWithpinningDebugImplementation + gplayDevTchapWithvoipWithpinningDebugImplementation + gplayTchapWithvoipWithpinningDebugImplementation + + + fdroidBtchapWithoutvoipWithpinningDebugImplementation + fdroidDevTchapWithoutvoipWithpinningDebugImplementation + fdroidTchapWithoutvoipWithpinningDebugImplementation + + gplayBtchapWithoutvoipWithpinningDebugImplementation + gplayDevTchapWithoutvoipWithpinningDebugImplementation + gplayTchapWithoutvoipWithpinningDebugImplementation + + + fdroidBtchapWithoutvoipWithoutpinningDebugImplementation + fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation + fdroidTchapWithoutvoipWithoutpinningDebugImplementation + + gplayBtchapWithoutvoipWithoutpinningDebugImplementation + gplayDevTchapWithoutvoipWithoutpinningDebugImplementation + gplayTchapWithoutvoipWithoutpinningDebugImplementation } dependencies { implementation project(':vector') implementation project(':vector-config') + debugImplementation project(':library:ui-styles') implementation libs.dagger.hilt implementation 'androidx.multidex:multidex:2.0.1' implementation "androidx.sharetarget:sharetarget:1.1.0" + // Flipper, debug builds only + debugImplementation(libs.flipper.flipper) { + exclude group: 'com.facebook.fbjni', module: 'fbjni' + } + debugImplementation(libs.flipper.flipperNetworkPlugin) { + exclude group: 'com.facebook.fbjni', module: 'fbjni' + } + debugImplementation 'com.facebook.soloader:soloader:0.10.4' + debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" + + gplayImplementation "com.google.android.gms:play-services-location:20.0.0" + // UnifiedPush gplay flavor only + gplayImplementation('com.google.firebase:firebase-messaging:23.0.8') { + exclude group: 'com.google.firebase', module: 'firebase-core' + exclude group: 'com.google.firebase', module: 'firebase-analytics' + exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' + } + + // Nightly + // API-only library + gplayImplementation libs.google.appdistributionApi + // Full SDK implementation + gplayImplementation libs.google.appdistribution + + // OSS License, gplay flavor only + gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' kapt libs.dagger.hiltCompiler + kapt libs.airbnb.epoxyProcessor + + // Tchap: We had to exclude fbjni for withVoip, the library is already include in jitsi library + // Flipper, debug builds only + gplayBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + gplayTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + gplayDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + + fdroidBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + fdroidTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + fdroidDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + + gplayBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + gplayDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + gplayTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + gplayTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + + fdroidBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + fdroidDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + fdroidTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + fdroidTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } + + gplayBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + gplayBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + gplayTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + gplayTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + gplayDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + gplayDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + + fdroidBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + fdroidBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + fdroidTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + fdroidTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) + fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + + gplayBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + gplayBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + gplayTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + gplayTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + gplayDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + gplayDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + fdroidBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + fdroidBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + fdroidTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + fdroidTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + fdroidDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) + fdroidDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) + + debugImplementation 'com.facebook.soloader:soloader:0.10.4' + debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" + + // Activate when you want to check for leaks, from time to time. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner @@ -438,7 +600,8 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" debugImplementation libs.androidx.fragmentTesting + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' } diff --git a/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index da13e49e84..901ef8e4c1 100644 --- a/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector-app/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -225,8 +225,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() { // Wait until local secrets are known (gossip) withIdlingResource(allSecretsKnownIdling(uiSession)) { - onView(withId(R.id.groupToolbarAvatarImageView)) - .perform(click()) + onView(withId(R.id.roomListContainer)) + .check(matches(isDisplayed())) } } diff --git a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt index 48fc1343b1..d8873a71a4 100644 --- a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt +++ b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt @@ -20,11 +20,13 @@ import androidx.test.platform.app.InstrumentationRegistry import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.SyncConfig fun getMatrixInstance(): Matrix { val context = InstrumentationRegistry.getInstrumentation().targetContext val configuration = MatrixConfiguration( - roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context) + roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context), + syncConfig = SyncConfig(longPollTimeout = 5_000L), ) return Matrix(context, configuration) } diff --git a/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt index 068c9fb646..5e131479bf 100644 --- a/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt +++ b/vector-app/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -92,7 +92,6 @@ private fun useMediaStoreScreenshotStorage( } } -@Suppress("DEPRECATION") private fun usePublicExternalScreenshotStorage( contentValues: ContentValues, contentResolver: ContentResolver, diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index b70fcfec25..d9dfb0facf 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -50,7 +50,7 @@ import im.vector.app.withIdlingResource import timber.log.Timber class ElementRobot( - private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(false) + private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(true) ) { fun onboarding(block: OnboardingRobot.() -> Unit) { block(OnboardingRobot()) @@ -110,9 +110,6 @@ class ElementRobot( closeSoftKeyboard() block(NewDirectMessageRobot()) pressBack() - if (labsPreferences.isNewAppLayoutEnabled) { - pressBack() // close create dialog - } waitUntilViewVisible(withId(R.id.roomListContainer)) } @@ -121,9 +118,6 @@ class ElementRobot( clickOn(R.id.bottom_action_rooms) } RoomListRobot(labsPreferences).newRoom { block() } - if (labsPreferences.isNewAppLayoutEnabled) { - pressBack() // close create dialog - } waitUntilViewVisible(withId(R.id.roomListContainer)) } diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt index 2c57dd058d..62c34e1b66 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/RoomSettingsRobot.kt @@ -34,18 +34,18 @@ class RoomSettingsRobot { fun crawl() { // Room settings - clickListItem(R.id.matrixProfileRecyclerView, 3) + clickListItem(R.id.matrixProfileRecyclerView, 4) navigateToRoomParameters() pressBack() // Notifications - clickListItem(R.id.matrixProfileRecyclerView, 5) + clickListItem(R.id.matrixProfileRecyclerView, 6) pressBack() assertDisplayed(R.id.roomProfileAvatarView) // People - clickListItem(R.id.matrixProfileRecyclerView, 7) + clickListItem(R.id.matrixProfileRecyclerView, 8) assertDisplayed(R.id.inviteUsersButton) navigateToRoomPeople() // Fab @@ -56,7 +56,7 @@ class RoomSettingsRobot { assertDisplayed(R.id.roomProfileAvatarView) // Uploads - clickListItem(R.id.matrixProfileRecyclerView, 9) + clickListItem(R.id.matrixProfileRecyclerView, 10) // File tab clickOn(R.string.uploads_files_title) waitUntilViewVisible(withText(R.string.uploads_media_title)) @@ -73,12 +73,12 @@ class RoomSettingsRobot { // Advanced // Room addresses - clickListItem(R.id.matrixProfileRecyclerView, 15) + clickListItem(R.id.matrixProfileRecyclerView, 16) waitUntilViewVisible(withText(R.string.room_alias_published_alias_title)) pressBack() // Room permissions - clickListItem(R.id.matrixProfileRecyclerView, 17) + clickListItem(R.id.matrixProfileRecyclerView, 18) waitUntilViewVisible(withText(R.string.room_permissions_change_room_avatar)) clickOn(R.string.room_permissions_change_room_avatar) waitUntilDialogVisible(withId(android.R.id.button2)) @@ -95,7 +95,7 @@ class RoomSettingsRobot { } private fun leaveRoom(block: DialogRobot.() -> Unit) { - clickListItem(R.id.matrixProfileRecyclerView, 13) + clickListItem(R.id.matrixProfileRecyclerView, 14) waitUntilDialogVisible(withId(android.R.id.button2)) val dialogRobot = DialogRobot() block(dialogRobot) diff --git a/vector-app/src/debug/AndroidManifest.xml b/vector-app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..a7867f4081 --- /dev/null +++ b/vector-app/src/debug/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt similarity index 99% rename from vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index eaaf021989..005e9c499b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -34,13 +34,13 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast -import im.vector.app.databinding.ActivityDebugMenuBinding import im.vector.app.features.debug.analytics.DebugAnalyticsActivity import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity import im.vector.app.features.debug.leak.DebugMemoryLeaksActivity import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity import im.vector.app.features.qrcode.QrCodeScannerActivity +import im.vector.application.databinding.ActivityDebugMenuBinding import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkDefaultActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkTestActivity import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkVectorActivity diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt similarity index 97% rename from vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt index 0f00f2daa5..a9be5512e4 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt @@ -23,13 +23,13 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.registerForPermissionsResult -import im.vector.app.databinding.ActivityDebugPermissionBinding +import im.vector.application.R +import im.vector.application.databinding.ActivityDebugPermissionBinding import timber.log.Timber @AndroidEntryPoint diff --git a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt similarity index 97% rename from vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt index 59c60e0e15..6e94bce00a 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt @@ -20,9 +20,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import im.vector.app.R -import im.vector.app.databinding.ActivityTestLinkifyBinding -import im.vector.app.databinding.ItemTestLinkifyBinding +import im.vector.application.R +import im.vector.application.databinding.ActivityTestLinkifyBinding +import im.vector.application.databinding.ItemTestLinkifyBinding class TestLinkifyActivity : AppCompatActivity() { diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt similarity index 97% rename from vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt index eb23fe6383..0fa11d7220 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt @@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.toOnOff import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentDebugAnalyticsBinding +import im.vector.application.databinding.FragmentDebugAnalyticsBinding import me.gujun.android.span.span class DebugAnalyticsFragment : VectorBaseFragment() { diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt b/vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/DebugModule.kt b/vector-app/src/debug/java/im/vector/app/features/debug/di/DebugModule.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/di/DebugModule.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/di/DebugModule.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt b/vector-app/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt b/vector-app/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt similarity index 94% rename from vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt index 1e9b88c048..38765bfa9b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt @@ -23,9 +23,9 @@ import android.widget.Spinner import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.application.R @EpoxyModelClass abstract class BooleanFeatureItem : VectorEpoxyModel(R.layout.item_feature) { @@ -70,8 +70,8 @@ abstract class BooleanFeatureItem : VectorEpoxyModel( } class Holder : VectorEpoxyHolder() { - val label by bind(im.vector.app.R.id.feature_label) - val optionsSpinner by bind(im.vector.app.R.id.feature_options) + val label by bind(R.id.feature_label) + val optionsSpinner by bind(R.id.feature_options) } interface Listener { diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt similarity index 96% rename from vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 9b2711a8c3..b927d66b69 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -80,11 +80,6 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.forceUsageOfOpusEncoder, factory = VectorFeatures::forceUsageOfOpusEncoder ), - createBooleanFeature( - label = "Start DM on first message", - key = DebugFeatureKeys.startDmOnFirstMsg, - factory = VectorFeatures::shouldStartDmOnFirstMessage - ), createBooleanFeature( label = "Enable New App Layout", key = DebugFeatureKeys.newAppLayoutEnabled, @@ -95,6 +90,11 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.newDeviceManagementEnabled, factory = VectorFeatures::isNewDeviceManagementEnabled ), + createBooleanFeature( + label = "Enable Voice Broadcast", + key = DebugFeatureKeys.voiceBroadcastEnabled, + factory = VectorFeatures::isVoiceBroadcastEnabled + ), ) ) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt similarity index 96% rename from vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index bb4cae3201..c347accfc3 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -73,15 +73,15 @@ class DebugVectorFeatures( override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder) ?: vectorFeatures.forceUsageOfOpusEncoder() - override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg) - ?: vectorFeatures.shouldStartDmOnFirstMessage() - override fun isNewAppLayoutFeatureEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled) ?: vectorFeatures.isNewAppLayoutFeatureEnabled() override fun isNewDeviceManagementEnabled(): Boolean = read(DebugFeatureKeys.newDeviceManagementEnabled) ?: vectorFeatures.isNewDeviceManagementEnabled() + override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) + ?: vectorFeatures.isVoiceBroadcastEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -143,4 +143,5 @@ object DebugFeatureKeys { val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg") val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled") val newDeviceManagementEnabled = booleanPreferencesKey("new-device-management-enabled") + val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt similarity index 92% rename from vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt index 5e16182f3c..57138b9a47 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt @@ -66,13 +66,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides { suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) { val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null)) context.dataStore.edit { settings -> - when (capabilitiesOverride.canChangeDisplayName) { + when (val canChangeDisplayName = capabilitiesOverride.canChangeDisplayName) { null -> settings.remove(forceCanChangeDisplayName) - else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName + else -> settings[forceCanChangeDisplayName] = canChangeDisplayName } - when (capabilitiesOverride.canChangeAvatar) { + when (val canChangeAvatar = capabilitiesOverride.canChangeAvatar) { null -> settings.remove(forceCanChangeAvatar) - else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar + else -> settings[forceCanChangeAvatar] = canChangeAvatar } } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt similarity index 94% rename from vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt index 5231e591da..00f74515cc 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt @@ -23,9 +23,9 @@ import android.widget.Spinner import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.application.R @EpoxyModelClass abstract class EnumFeatureItem : VectorEpoxyModel(R.layout.item_feature) { @@ -70,8 +70,8 @@ abstract class EnumFeatureItem : VectorEpoxyModel(R.layo } class Holder : VectorEpoxyHolder() { - val label by bind(im.vector.app.R.id.feature_label) - val optionsSpinner by bind(im.vector.app.R.id.feature_options) + val label by bind(R.id.feature_label) + val optionsSpinner by bind(R.id.feature_options) } interface Listener { diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt similarity index 96% rename from vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt index 2abf6487e2..e9afa9aea9 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksFragment.kt @@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.epoxy.onClick import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentDebugMemoryLeaksBinding +import im.vector.application.databinding.FragmentDebugMemoryLeaksBinding @AndroidEntryPoint class DebugMemoryLeaksFragment : diff --git a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewActions.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewActions.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewActions.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewActions.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt similarity index 98% rename from vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt index 5432cb0888..26eb1c1025 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * 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. diff --git a/vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewState.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewState.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewState.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksViewState.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiController.kt b/vector-app/src/debug/java/im/vector/app/features/debug/sas/SasEmojiController.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiController.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/sas/SasEmojiController.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt b/vector-app/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt similarity index 98% rename from vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt index 179ee35693..bbc438e4b2 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt @@ -21,9 +21,9 @@ import android.widget.TextView import androidx.core.content.ContextCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.application.R import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt similarity index 97% rename from vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt index be3d41e0e1..020c228521 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt @@ -25,8 +25,8 @@ import android.view.ViewGroup import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentDebugPrivateSettingsBinding import im.vector.app.features.home.room.list.home.release.ReleaseNotesActivity +import im.vector.application.databinding.FragmentDebugPrivateSettingsBinding class DebugPrivateSettingsFragment : VectorBaseFragment() { diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt similarity index 97% rename from vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt index 7f510ee5e9..2800b7bd8d 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt @@ -24,7 +24,7 @@ import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.LinearLayout -import im.vector.app.databinding.ViewBooleanDropdownBinding +import im.vector.application.databinding.ViewBooleanDropdownBinding class OverrideDropdownView @JvmOverloads constructor( context: Context, diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt rename to vector-app/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt diff --git a/vector/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt b/vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt rename to vector-app/src/debug/java/im/vector/app/flipper/VectorFlipperProxy.kt diff --git a/vector/src/debug/java/im/vector/app/leakcanary/LeakCanaryLeakDetector.kt b/vector-app/src/debug/java/im/vector/app/leakcanary/LeakCanaryLeakDetector.kt similarity index 100% rename from vector/src/debug/java/im/vector/app/leakcanary/LeakCanaryLeakDetector.kt rename to vector-app/src/debug/java/im/vector/app/leakcanary/LeakCanaryLeakDetector.kt diff --git a/vector/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt b/vector-app/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt similarity index 85% rename from vector/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt rename to vector-app/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt index 550dc055d9..4edbdd0591 100644 --- a/vector/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt +++ b/vector-app/src/debug/java/im/vector/app/receivers/VectorDebugReceiver.kt @@ -23,7 +23,7 @@ import android.content.IntentFilter import android.content.SharedPreferences import androidx.core.content.edit import im.vector.app.core.debug.DebugReceiver -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.utils.lsFiles import timber.log.Timber import javax.inject.Inject @@ -31,7 +31,10 @@ import javax.inject.Inject /** * Receiver to handle some command from ADB */ -class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugReceiver { +class VectorDebugReceiver @Inject constructor( + @DefaultPreferences + private val sharedPreferences: SharedPreferences, +) : BroadcastReceiver(), DebugReceiver { override fun register(context: Context) { context.registerReceiver(this, getIntentFilter(context)) @@ -47,14 +50,14 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece intent.action?.let { when { it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context) - it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences(context) - it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken(context) + it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences() + it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken() } } } - private fun dumpPreferences(context: Context) { - logPrefs("DefaultSharedPreferences", DefaultSharedPreferences.getInstance(context)) + private fun dumpPreferences() { + logPrefs("DefaultSharedPreferences", sharedPreferences) } private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) { @@ -67,8 +70,8 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece } } - private fun alterScalarToken(context: Context) { - DefaultSharedPreferences.getInstance(context).edit { + private fun alterScalarToken() { + sharedPreferences.edit { // putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token") } } diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector-app/src/debug/res/layout/activity_debug_menu.xml similarity index 100% rename from vector/src/debug/res/layout/activity_debug_menu.xml rename to vector-app/src/debug/res/layout/activity_debug_menu.xml diff --git a/vector/src/debug/res/layout/activity_debug_permission.xml b/vector-app/src/debug/res/layout/activity_debug_permission.xml similarity index 100% rename from vector/src/debug/res/layout/activity_debug_permission.xml rename to vector-app/src/debug/res/layout/activity_debug_permission.xml diff --git a/vector/src/debug/res/layout/activity_test_linkify.xml b/vector-app/src/debug/res/layout/activity_test_linkify.xml similarity index 100% rename from vector/src/debug/res/layout/activity_test_linkify.xml rename to vector-app/src/debug/res/layout/activity_test_linkify.xml diff --git a/vector/src/debug/res/layout/demo_theme_sample.xml b/vector-app/src/debug/res/layout/demo_theme_sample.xml similarity index 100% rename from vector/src/debug/res/layout/demo_theme_sample.xml rename to vector-app/src/debug/res/layout/demo_theme_sample.xml diff --git a/vector/src/debug/res/layout/demo_themes.xml b/vector-app/src/debug/res/layout/demo_themes.xml similarity index 100% rename from vector/src/debug/res/layout/demo_themes.xml rename to vector-app/src/debug/res/layout/demo_themes.xml diff --git a/vector/src/debug/res/layout/fragment_debug_analytics.xml b/vector-app/src/debug/res/layout/fragment_debug_analytics.xml similarity index 100% rename from vector/src/debug/res/layout/fragment_debug_analytics.xml rename to vector-app/src/debug/res/layout/fragment_debug_analytics.xml diff --git a/vector/src/debug/res/layout/fragment_debug_memory_leaks.xml b/vector-app/src/debug/res/layout/fragment_debug_memory_leaks.xml similarity index 100% rename from vector/src/debug/res/layout/fragment_debug_memory_leaks.xml rename to vector-app/src/debug/res/layout/fragment_debug_memory_leaks.xml diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector-app/src/debug/res/layout/fragment_debug_private_settings.xml similarity index 100% rename from vector/src/debug/res/layout/fragment_debug_private_settings.xml rename to vector-app/src/debug/res/layout/fragment_debug_private_settings.xml diff --git a/vector/src/debug/res/layout/item_feature.xml b/vector-app/src/debug/res/layout/item_feature.xml similarity index 100% rename from vector/src/debug/res/layout/item_feature.xml rename to vector-app/src/debug/res/layout/item_feature.xml diff --git a/vector/src/debug/res/layout/item_sas_emoji.xml b/vector-app/src/debug/res/layout/item_sas_emoji.xml similarity index 100% rename from vector/src/debug/res/layout/item_sas_emoji.xml rename to vector-app/src/debug/res/layout/item_sas_emoji.xml diff --git a/vector/src/debug/res/layout/item_test_linkify.xml b/vector-app/src/debug/res/layout/item_test_linkify.xml similarity index 100% rename from vector/src/debug/res/layout/item_test_linkify.xml rename to vector-app/src/debug/res/layout/item_test_linkify.xml diff --git a/vector/src/debug/res/layout/view_boolean_dropdown.xml b/vector-app/src/debug/res/layout/view_boolean_dropdown.xml similarity index 100% rename from vector/src/debug/res/layout/view_boolean_dropdown.xml rename to vector-app/src/debug/res/layout/view_boolean_dropdown.xml diff --git a/vector/src/debug/res/values/strings.xml b/vector-app/src/debug/res/values/strings.xml similarity index 100% rename from vector/src/debug/res/values/strings.xml rename to vector-app/src/debug/res/values/strings.xml diff --git a/vector/src/debug/res/xml/shortcuts.xml b/vector-app/src/debug/res/xml/shortcuts.xml similarity index 100% rename from vector/src/debug/res/xml/shortcuts.xml rename to vector-app/src/debug/res/xml/shortcuts.xml diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector-app/src/fdroid/AndroidManifest.xml similarity index 78% rename from vector/src/fdroid/AndroidManifest.xml rename to vector-app/src/fdroid/AndroidManifest.xml index 15db89ca13..354d450958 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector-app/src/fdroid/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -15,7 +14,7 @@ @@ -24,12 +23,12 @@ diff --git a/vector/src/fdroid/java/im/vector/app/di/FlavorModule.kt b/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/di/FlavorModule.kt rename to vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt diff --git a/vector/src/fdroid/java/im/vector/app/di/NotificationTestModule.kt b/vector-app/src/fdroid/java/im/vector/app/di/NotificationTestModule.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/di/NotificationTestModule.kt rename to vector-app/src/fdroid/java/im/vector/app/di/NotificationTestModule.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/package-info.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/package-info.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/package-info.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/package-info.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt similarity index 95% rename from vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index bd1e0eb0ee..bccbf42e92 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -16,6 +16,7 @@ package im.vector.app.fdroid.receiver +import android.annotation.SuppressLint import android.app.AlarmManager import android.app.PendingIntent import android.content.BroadcastReceiver @@ -65,6 +66,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { companion object { private const val REQUEST_CODE = 0 + @SuppressLint("WrongConstant") // PendingIntentCompat.FLAG_IMMUTABLE is a false positive fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int, clock: Clock) { // Reschedule Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds") @@ -87,6 +89,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { } } + @SuppressLint("WrongConstant") // PendingIntentCompat.FLAG_IMMUTABLE is a false positive fun cancelAlarm(context: Context) { Timber.v("## Sync: Cancel alarm for background sync") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt b/vector-app/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt rename to vector-app/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt rename to vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FdroidNotificationTroubleshootTestManagerFactory.kt b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidNotificationTroubleshootTestManagerFactory.kt similarity index 100% rename from vector/src/fdroid/java/im/vector/app/push/fcm/FdroidNotificationTroubleshootTestManagerFactory.kt rename to vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidNotificationTroubleshootTestManagerFactory.kt diff --git a/vector/src/gplay/java/im/vector/app/GoogleFlavorLegals.kt b/vector-app/src/gplay/java/im/vector/app/GoogleFlavorLegals.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/GoogleFlavorLegals.kt rename to vector-app/src/gplay/java/im/vector/app/GoogleFlavorLegals.kt diff --git a/vector/src/gplay/java/im/vector/app/di/FlavorModule.kt b/vector-app/src/gplay/java/im/vector/app/di/FlavorModule.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/di/FlavorModule.kt rename to vector-app/src/gplay/java/im/vector/app/di/FlavorModule.kt diff --git a/vector/src/gplay/java/im/vector/app/di/NotificationTestModule.kt b/vector-app/src/gplay/java/im/vector/app/di/NotificationTestModule.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/di/NotificationTestModule.kt rename to vector-app/src/gplay/java/im/vector/app/di/NotificationTestModule.kt diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt rename to vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt b/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt rename to vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt rename to vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt diff --git a/vector/src/gplay/java/im/vector/app/gplay/package-info.kt b/vector-app/src/gplay/java/im/vector/app/gplay/package-info.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/gplay/package-info.kt rename to vector-app/src/gplay/java/im/vector/app/gplay/package-info.kt diff --git a/vector/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt rename to vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt similarity index 95% rename from vector/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt rename to vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt index d64847c124..7cf90cf874 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt @@ -17,6 +17,7 @@ package im.vector.app.push.fcm import android.app.Activity import android.content.Context +import android.content.SharedPreferences import android.widget.Toast import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult @@ -24,7 +25,7 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import timber.log.Timber @@ -35,14 +36,13 @@ import javax.inject.Inject * It has an alter ego in the fdroid variant. */ class GoogleFcmHelper @Inject constructor( - context: Context, + @DefaultPreferences + private val sharedPrefs: SharedPreferences, ) : FcmHelper { companion object { private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" } - private val sharedPrefs = DefaultSharedPreferences.getInstance(context) - override fun isFirebaseAvailable(): Boolean = true override fun getFcmToken(): String? { diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/GoogleNotificationTroubleshootTestManagerFactory.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleNotificationTroubleshootTestManagerFactory.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/push/fcm/GoogleNotificationTroubleshootTestManagerFactory.kt rename to vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleNotificationTroubleshootTestManagerFactory.kt diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt similarity index 100% rename from vector/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt rename to vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt diff --git a/vector-app/src/main/AndroidManifest.xml b/vector-app/src/main/AndroidManifest.xml index 84607cf3d7..2767b20404 100644 --- a/vector-app/src/main/AndroidManifest.xml +++ b/vector-app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + + + + + + + + + + + + diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index 5097b336d3..3a98bed9fd 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -53,7 +53,7 @@ import im.vector.app.core.resources.BuildMeta import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration -import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog +import im.vector.app.features.disclaimer.DisclaimerDialog import im.vector.app.features.invite.InvitesAcceptor import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.app.features.notifications.NotificationDrawerManager @@ -108,6 +108,8 @@ class VectorApplication : @Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var buildMeta: BuildMeta @Inject lateinit var leakDetector: LeakDetector + @Inject lateinit var vectorLocale: VectorLocale + @Inject lateinit var disclaimerDialog: DisclaimerDialog // font thread handler private var fontThreadHandler: Handler? = null @@ -160,7 +162,7 @@ class VectorApplication : R.array.com_google_android_gms_fonts_certs ) FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) - VectorLocale.init(this, buildMeta) + vectorLocale.init() ThemeUtils.init(this) vectorConfiguration.applyToApplicationContext() @@ -172,7 +174,7 @@ class VectorApplication : val sessionImported = legacySessionImporter.process() if (!sessionImported) { // Do not display the name change popup - doNotShowDisclaimerDialog(this) + disclaimerDialog.doNotShowDisclaimerDialog() } ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { diff --git a/vector-app/src/main/res/xml/backup_rules.xml b/vector-app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..fe10b316ed --- /dev/null +++ b/vector-app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/vector-app/src/main/res/xml/data_extraction_rules.xml b/vector-app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..fe61eb717c --- /dev/null +++ b/vector-app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/nightly/res/xml/shortcuts.xml b/vector-app/src/nightly/res/xml/shortcuts.xml similarity index 100% rename from vector/src/nightly/res/xml/shortcuts.xml rename to vector-app/src/nightly/res/xml/shortcuts.xml diff --git a/vector/src/release/java/im/vector/app/core/di/DebugModule.kt b/vector-app/src/release/java/im/vector/app/core/di/DebugModule.kt similarity index 100% rename from vector/src/release/java/im/vector/app/core/di/DebugModule.kt rename to vector-app/src/release/java/im/vector/app/core/di/DebugModule.kt diff --git a/vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt b/vector-app/src/release/java/im/vector/app/core/di/FeaturesModule.kt similarity index 100% rename from vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt rename to vector-app/src/release/java/im/vector/app/core/di/FeaturesModule.kt diff --git a/vector/src/release/java/im/vector/app/receivers/DebugReceiver.kt b/vector-app/src/release/java/im/vector/app/receivers/DebugReceiver.kt similarity index 100% rename from vector/src/release/java/im/vector/app/receivers/DebugReceiver.kt rename to vector-app/src/release/java/im/vector/app/receivers/DebugReceiver.kt diff --git a/vector/src/release/res/xml/shortcuts.xml b/vector-app/src/release/res/xml/shortcuts.xml similarity index 100% rename from vector/src/release/res/xml/shortcuts.xml rename to vector-app/src/release/res/xml/shortcuts.xml diff --git a/vector-config/src/main/java/im/vector/app/config/Analytics.kt b/vector-config/src/main/java/im/vector/app/config/Analytics.kt index 7fdc78dc8a..d944a84f94 100644 --- a/vector-config/src/main/java/im/vector/app/config/Analytics.kt +++ b/vector-config/src/main/java/im/vector/app/config/Analytics.kt @@ -27,9 +27,9 @@ sealed interface Analytics { object Disabled : Analytics /** - * Analytics integration via PostHog. + * Analytics integration via PostHog and Sentry. */ - data class PostHog( + data class Enabled( /** * The PostHog instance url. */ @@ -44,5 +44,15 @@ sealed interface Analytics { * A URL to more information about the analytics collection. */ val policyLink: String, + + /** + * The Sentry DSN url. + */ + val sentryDSN: String, + + /** + * Environment for Sentry. + */ + val sentryEnvironment: String ) : Analytics } diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index d48df65f6e..599a55ac85 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -42,8 +42,10 @@ true + true + true false - false + false true false diff --git a/vector/build.gradle b/vector/build.gradle index c66c02f28f..dc9e715311 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -24,6 +24,8 @@ project.android.buildTypes.all { buildType -> ] } +initScreenshotTests(project) + android { // Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use // Ref: https://issuetracker.google.com/issues/144111441 @@ -83,11 +85,6 @@ android { testCoverageEnabled = coverage.enableTestCoverage } } - // Tchap: No nightly -// nightly { -// initWith release -// matchingFallbacks = ['release'] -// } release { // Tchap: Show developer mode only in debug resValue "bool", "developer_mode_visible", "false" @@ -203,11 +200,6 @@ android { test { java.srcDirs += "src/sharedTest/java" } - // Add sourceSets for `release` version when building `nightly` - // Tchap: No nightly -// nightly { -// java.srcDirs += "src/release/java" -// } } buildFeatures { @@ -215,54 +207,16 @@ android { } } -// Tchap: Use custom configuration for Flipper library -configurations { - fdroidBtchapWithvoipWithoutpinningDebugImplementation - fdroidDevTchapWithvoipWithoutpinningDebugImplementation - fdroidTchapWithvoipWithoutpinningDebugImplementation - - gplayBtchapWithvoipWithoutpinningDebugImplementation - gplayDevTchapWithvoipWithoutpinningDebugImplementation - gplayTchapWithvoipWithoutpinningDebugImplementation - - - fdroidBtchapWithvoipWithpinningDebugImplementation - fdroidDevTchapWithvoipWithpinningDebugImplementation - fdroidTchapWithvoipWithpinningDebugImplementation - - gplayBtchapWithvoipWithpinningDebugImplementation - gplayDevTchapWithvoipWithpinningDebugImplementation - gplayTchapWithvoipWithpinningDebugImplementation - - - fdroidBtchapWithoutvoipWithpinningDebugImplementation - fdroidDevTchapWithoutvoipWithpinningDebugImplementation - fdroidTchapWithoutvoipWithpinningDebugImplementation - - gplayBtchapWithoutvoipWithpinningDebugImplementation - gplayDevTchapWithoutvoipWithpinningDebugImplementation - gplayTchapWithoutvoipWithpinningDebugImplementation - - - fdroidBtchapWithoutvoipWithoutpinningDebugImplementation - fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation - fdroidTchapWithoutvoipWithoutpinningDebugImplementation - - gplayBtchapWithoutvoipWithoutpinningDebugImplementation - gplayDevTchapWithoutvoipWithoutpinningDebugImplementation - gplayTchapWithoutvoipWithoutpinningDebugImplementation -} - dependencies { implementation project(":vector-config") api project(":matrix-sdk-android") implementation project(":matrix-sdk-android-flow") - implementation project(":library:jsonviewer") + implementation project(":library:external:jsonviewer") + implementation project(":library:external:diff-match-patch") implementation project(":library:ui-strings") implementation project(":library:ui-styles") implementation project(":library:core-utils") implementation project(":library:attachment-viewer") - implementation project(":library:diff-match-patch") implementation project(":library:multipicker") implementation libs.jetbrains.coroutinesCore @@ -314,12 +268,6 @@ dependencies { // Snap Helper https://github.com/rubensousa/GravitySnapHelper api 'com.github.rubensousa:gravitysnaphelper:2.2.2' - // Nightly - // API-only library - gplayImplementation libs.google.appdistributionApi - // Full SDK implementation - gplayImplementation libs.google.appdistribution - // Work api libs.androidx.work @@ -335,9 +283,12 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation libs.google.material - implementation 'me.gujun.android:span:1.7' + api('me.gujun.android:span:1.7') { + exclude group: 'com.android.support', module: 'support-annotations' + } implementation libs.markwon.core implementation libs.markwon.extLatex + implementation libs.markwon.imageGlide implementation libs.markwon.inlineParser implementation libs.markwon.html implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.5.2' @@ -364,7 +315,6 @@ dependencies { // Image Loading implementation libs.github.bigImageViewer implementation libs.github.glideImageLoader - implementation libs.github.progressPieIndicator implementation libs.github.glideImageViewFactory // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' @@ -383,21 +333,15 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation 'com.posthog.android:posthog:1.1.2' - - // UnifiedPush - implementation 'com.github.UnifiedPush:android-connector:2.0.1' - // UnifiedPush gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:23.0.8') { - exclude group: 'com.google.firebase', module: 'firebase-core' - exclude group: 'com.google.firebase', module: 'firebase-analytics' - exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' + implementation('com.posthog.android:posthog:1.1.2') { + exclude group: 'com.android.support', module: 'support-annotations' } + implementation libs.sentry.sentryAndroid - // OSS License, gplay flavor only - gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' + // UnifiedPush + implementation 'com.github.UnifiedPush:android-connector:2.1.0' - implementation "androidx.emoji2:emoji2:1.1.0" + implementation "androidx.emoji2:emoji2:1.2.0" // WebRTC // org.webrtc:google-webrtc is for development purposes only @@ -409,6 +353,11 @@ dependencies { exclude group: 'com.google.firebase' exclude group: 'com.google.android.gms' exclude group: 'com.android.installreferrer' + + // Exclude jitsi's android-scalablevideoview fork's support library + // The library exports a jetified artifact but doesn't remove the support library dependency + // https://github.com/MatrixFrog/Android-ScalableVideoView/blob/master/gradle.properties#L1 + exclude group: 'com.android.support', module: 'appcompat-v7' } // tchap @@ -418,13 +367,18 @@ dependencies { // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 implementation 'com.google.zxing:core:3.3.3' - implementation 'me.dm7.barcodescanner:zxing:1.9.13' + + // Excludes the legacy support library annotation usages + // https://github.com/dm77/barcodescanner/blob/d036996c8a6f36a68843ffe539c834c28944b2d5/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java#L4 + implementation ('me.dm7.barcodescanner:zxing:1.9.13') { + exclude group: 'com.android.support', module: 'support-v4' + } // Emoji Keyboard api libs.vanniktech.emojiMaterial api libs.vanniktech.emojiGoogle - implementation 'im.dlg:android-dialer:1.2.5' + implementation project(":library:external:dialpad") // JWT api libs.jsonwebtoken.jjwtApi @@ -435,19 +389,18 @@ dependencies { implementation 'commons-codec:commons-codec:1.15' // MapTiler - fdroidApi(libs.maplibre.androidSdk) { + api(libs.maplibre.androidSdk) { exclude group: 'com.google.android.gms', module: 'play-services-location' } - fdroidApi(libs.maplibre.pluginAnnotation) { + api(libs.maplibre.pluginAnnotation) { exclude group: 'com.google.android.gms', module: 'play-services-location' } - gplayApi libs.maplibre.androidSdk - gplayApi libs.maplibre.pluginAnnotation // TESTS testImplementation libs.tests.junit testImplementation libs.tests.kluent testImplementation libs.mockk.mockk + testImplementation libs.androidx.coreTesting // Plant Timber tree for test testImplementation libs.tests.timberJunitRule testImplementation libs.airbnb.mavericksTesting @@ -455,92 +408,6 @@ dependencies { exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" } - - // Tchap: We had to exclude fbjni for withVoip, the library is already include in jitsi library - // Flipper, debug builds only - gplayBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - gplayTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - gplayDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - - fdroidBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidBtchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - fdroidTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - fdroidDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidDevTchapWithvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - - gplayBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - gplayDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - gplayTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - gplayTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - - fdroidBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidBtchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - fdroidDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidDevTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - fdroidTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - fdroidTchapWithvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - - gplayBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - gplayBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - gplayTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - gplayTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - gplayDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - gplayDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - - fdroidBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - fdroidBtchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - fdroidTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - fdroidTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipper) - fdroidDevTchapWithoutvoipWithoutpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - - gplayBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - gplayBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - gplayTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - gplayTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - gplayDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - gplayDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - fdroidBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - fdroidBtchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - fdroidTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - fdroidTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - fdroidDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipper) - fdroidDevTchapWithoutvoipWithpinningDebugImplementation(libs.flipper.flipperNetworkPlugin) - - debugImplementation 'com.facebook.soloader:soloader:0.10.4' - debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" - - // Activate when you want to check for leaks, from time to time. - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' - androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner androidTestImplementation libs.androidx.testRules @@ -562,5 +429,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" } diff --git a/vector/src/androidTest/AndroidManifest.xml b/vector/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..5c3b99d4d1 --- /dev/null +++ b/vector/src/androidTest/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt b/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt deleted file mode 100644 index 97333b7c98..0000000000 --- a/vector/src/androidTest/java/im/vector/app/AndroidVersionTestOverrider.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app - -import android.os.Build -import java.lang.reflect.Field - -/** - * Used to override [Build.VERSION.SDK_INT]. Ideally an interface should be used instead, but that approach forces us to either add suppress lint annotations - * and potentially miss an API version issue or write a custom lint rule, which seems like an overkill. - */ -object AndroidVersionTestOverrider { - - private var initialValue: Int? = null - - fun override(newVersion: Int) { - if (initialValue == null) { - initialValue = Build.VERSION.SDK_INT - } - val field = Build.VERSION::class.java.getField("SDK_INT") - setStaticField(field, newVersion) - } - - fun restore() { - initialValue?.let { override(it) } - } - - private fun setStaticField(field: Field, value: Any) { - field.isAccessible = true - field.set(null, value) - } -} diff --git a/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt index ddf89b5e46..b3f9c65800 100644 --- a/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt +++ b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt @@ -18,8 +18,6 @@ package im.vector.app import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider -class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { - var value: Int = 0 - +class TestBuildVersionSdkIntProvider(var value: Int = 0) : BuildVersionSdkIntProvider { override fun get() = value } diff --git a/vector/src/androidTest/java/im/vector/app/features/RoomMemberListControllerTest.kt b/vector/src/androidTest/java/im/vector/app/features/RoomMemberListControllerTest.kt new file mode 100644 index 0000000000..527751aae2 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/RoomMemberListControllerTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features + +import com.airbnb.mvrx.Success +import im.vector.app.core.epoxy.profiles.ProfileMatrixItemWithPowerLevelWithPresence +import im.vector.app.features.roomprofile.members.RoomMemberListCategories +import im.vector.app.features.roomprofile.members.RoomMemberListController +import im.vector.app.features.roomprofile.members.RoomMemberListViewState +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Ignore +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class RoomMemberListControllerTest { + + @Test + @Ignore("Too flaky") + fun testControllerUserVerificationLevel() = runTest { + val roomListController = RoomMemberListController( + avatarRenderer = mockk { + }, + stringProvider = mockk { + every { getString(any()) } answers { + this.args[0].toString() + } + }, + colorProvider = mockk { + every { getColorFromAttribute(any()) } returns 0x0 + }, + roomMemberSummaryFilter = mockk(relaxed = true) { + every { test(any()) } returns true + } + ) + + val fakeRoomSummary = RoomSummary( + roomId = "!roomId", + displayName = "Fake Room", + topic = "A topic", + isEncrypted = true, + encryptionEventTs = 0, + typingUsers = emptyList(), + ) + + val state = RoomMemberListViewState( + roomId = "!roomId", + roomSummary = Success(fakeRoomSummary), + areAllMembersLoaded = true, + roomMemberSummaries = Success( + listOf( + RoomMemberListCategories.USER to listOf( + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@alice:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@bob:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@carl:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@massy:example.com" + ) + ) + ) + ), + trustLevelMap = Success( + mapOf( + "@alice:example.com" to UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY, + "@bob:example.com" to UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED, + "@carl:example.com" to UserVerificationLevel.WAS_NEVER_VERIFIED, + "@massy:example.com" to UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED, + ) + ) + ) + + suspendCoroutine { continuation -> + roomListController.setData(state) + roomListController.addModelBuildListener { + continuation.resume(it) + } + } + + val models = roomListController.adapter.copyOfModels + + val profileItems = models.filterIsInstance() + + profileItems.firstOrNull { + it.matrixItem.id == "@alice:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + + profileItems.firstOrNull { + it.matrixItem.id == "@bob:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED + + profileItems.firstOrNull { + it.matrixItem.id == "@carl:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.WAS_NEVER_VERIFIED + + profileItems.firstOrNull { + it.matrixItem.id == "@massy:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt index 41c0f51322..a2e489dd70 100644 --- a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.html import androidx.core.text.toSpannable import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.toTestSpan import im.vector.app.features.settings.VectorPreferences @@ -36,11 +37,13 @@ class EventHtmlRendererTest { private val fakeVectorPreferences = mockk().also { every { it.latexMathsIsEnabled() } returns false } + private val fakeSessionHolder = mockk() private val renderer = EventHtmlRenderer( MatrixHtmlPluginConfigure(ColorProvider(context), context.resources), context, - fakeVectorPreferences + fakeVectorPreferences, + fakeSessionHolder, ) @Test diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt index 44c5db89c8..8c50806fd9 100644 --- a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/migrations/LegacyPinCodeMigratorTests.kt @@ -25,6 +25,7 @@ import android.security.keystore.KeyProperties import android.util.Base64 import androidx.preference.PreferenceManager import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE @@ -32,7 +33,6 @@ import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LE import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every -import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.runBlocking @@ -42,7 +42,6 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Test import org.matrix.android.sdk.api.securestorage.SecretStoringUtils -import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import java.math.BigInteger import java.security.KeyFactory import java.security.KeyPairGenerator @@ -66,9 +65,7 @@ class LegacyPinCodeMigratorTests { SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context)) ) private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) } - private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk { - every { get() } returns Build.VERSION_CODES.M - } + private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider(Build.VERSION_CODES.M) private val secretStoringUtils: SecretStoringUtils = spyk( SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) ) @@ -125,26 +122,18 @@ class LegacyPinCodeMigratorTests { @Test fun migratePinCodeM() = runTest { - val pinCode = "1234" - saveLegacyPinCode(pinCode) - - legacyPinCodeMigrator.migrate() - - coVerify { legacyPinCodeMigrator.getDecryptedPinCode() } - verify { secretStoringUtils.securelyStoreBytes(any(), any()) } - coVerify { pinCodeStore.savePinCode(any()) } - verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } - - val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias)) - decodedPinCode shouldBeEqualTo pinCode - keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false - keyStore.containsAlias(alias) shouldBe true + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + migratePinCode() } @Test fun migratePinCodeL() = runTest { + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + migratePinCode() + } + + private suspend fun migratePinCode() { val pinCode = "1234" - every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP saveLegacyPinCode(pinCode) legacyPinCodeMigrator.migrate() @@ -163,7 +152,7 @@ class LegacyPinCodeMigratorTests { private fun generateLegacyKey() { if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return - if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { generateLegacyKeyM() } else { generateLegacyKeyL() @@ -206,7 +195,7 @@ class LegacyPinCodeMigratorTests { generateLegacyKey() val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey val cipher = getLegacyCipher() - if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded)) val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT) cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec) @@ -219,14 +208,15 @@ class LegacyPinCodeMigratorTests { } private fun getLegacyCipher(): Cipher { - return when (buildVersionSdkIntProvider.get()) { - Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL() - else -> getCipherM() + return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { + getCipherM() + } else { + getCipherL() } } private fun getCipherL(): Cipher { - val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround" + val provider = if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) "AndroidKeyStoreBCWorkaround" else "AndroidOpenSSL" val transformation = "RSA/ECB/PKCS1Padding" return Cipher.getInstance(transformation, provider) } diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt index 1687ee4388..374396f60b 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderLTests.kt @@ -19,24 +19,25 @@ package im.vector.app.features.voice import android.Manifest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import io.mockk.spyk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldExist import org.amshove.kluent.shouldNotBeNull import org.amshove.kluent.shouldNotExist +import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.File +@Ignore("Disabled temporarily so that we can unblock other PRs.") class VoiceRecorderLTests { @get:Rule val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO) private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val recorder = spyk(VoiceRecorderL(context, Dispatchers.IO)) + private val recorder = VoiceRecorderL(context, Dispatchers.IO, createFakeOpusEncoder()) @Test fun startRecordCreatesOggFile() = with(recorder) { diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt index 65f81b145b..0610496dfe 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderProviderTests.kt @@ -18,41 +18,36 @@ package im.vector.app.features.voice import android.os.Build import androidx.test.platform.app.InstrumentationRegistry -import im.vector.app.AndroidVersionTestOverrider +import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.features.DefaultVectorFeatures import io.mockk.every import io.mockk.spyk import org.amshove.kluent.shouldBeInstanceOf -import org.junit.After import org.junit.Test class VoiceRecorderProviderTests { private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures())) - - @After - fun tearDown() { - AndroidVersionTestOverrider.restore() - } + private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() + private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures(), buildVersionSdkIntProvider)) @Test fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() { - AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) + buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q every { provider.hasOpusEncoder() } returns true provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class) } @Test fun provideVoiceRecorderOnAndroidQButNoCodecReturnsLRecorder() { - AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) + buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q every { provider.hasOpusEncoder() } returns false provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class) } @Test fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() { - AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP) + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class) } } diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt index 4275ae89b3..75303556b2 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTestExt.kt @@ -17,6 +17,7 @@ package im.vector.app.features.voice import im.vector.app.core.utils.waitUntil +import im.vector.app.test.fakes.FakeOggOpusEncoder import org.amshove.kluent.shouldExist import org.amshove.kluent.shouldNotBeNull import java.io.File @@ -34,3 +35,5 @@ suspend fun VoiceRecorder.waitUntilRecordingFileExists(timeout: Duration = 1.sec } return getVoiceMessageFile() } + +internal fun createFakeOpusEncoder() = FakeOggOpusEncoder().apply { createEmptyFileOnInit() } diff --git a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt index 7feeff83cb..72d959f3dd 100644 --- a/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt +++ b/vector/src/androidTest/java/im/vector/app/features/voice/VoiceRecorderTests.kt @@ -29,7 +29,7 @@ import java.io.File class VoiceRecorderTests { private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val voiceRecorder = VoiceRecorderL(context, Dispatchers.IO) + private val voiceRecorder = VoiceRecorderL(context, Dispatchers.IO, createFakeOpusEncoder()) private val audioDirectory = File(context.cacheDir, "voice_records") @After diff --git a/vector/src/androidTest/java/im/vector/app/test/fakes/FakeOggOpusEncoder.kt b/vector/src/androidTest/java/im/vector/app/test/fakes/FakeOggOpusEncoder.kt new file mode 100644 index 0000000000..a13c8dbb78 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/test/fakes/FakeOggOpusEncoder.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import io.element.android.opusencoder.OggOpusEncoder +import io.mockk.every +import io.mockk.mockk +import java.io.File + +class FakeOggOpusEncoder : OggOpusEncoder by mockk() { + + init { + every { init(any(), any()) } returns 0 + every { setBitrate(any()) } returns 0 + every { encode(any(), any()) } returns 0 + every { release() } answers {} + } + + fun createEmptyFileOnInit() { + every { init(any(), any()) } answers { + val filePath = arg(0) + if (File(filePath).createNewFile()) 0 else 1 + } + } +} diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml deleted file mode 100644 index 94fdb1b389..0000000000 --- a/vector/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index de061df0a0..7342d9c874 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -35,14 +35,6 @@ - - - - @@ -77,6 +69,9 @@ + + + - - - - - - - - - - - - - + - - - + + + + + + diff --git a/vector/src/main/java/fr/gouv/tchap/core/dialogs/InviteByEmailDialog.kt b/vector/src/main/java/fr/gouv/tchap/core/dialogs/InviteByEmailDialog.kt index c1bf5f2e1f..7a0a32a3a8 100644 --- a/vector/src/main/java/fr/gouv/tchap/core/dialogs/InviteByEmailDialog.kt +++ b/vector/src/main/java/fr/gouv/tchap/core/dialogs/InviteByEmailDialog.kt @@ -24,7 +24,6 @@ import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.isEmail import im.vector.app.databinding.DialogInviteByIdBinding -import im.vector.app.features.settings.VectorLocale class InviteByEmailDialog( private val activity: Activity @@ -42,7 +41,7 @@ class InviteByEmailDialog( .setTitle(R.string.tchap_people_search_invite_by_id_dialog_title) .setView(dialogLayout) .setPositiveButton(R.string.action_invite) { _, _ -> - val text = views.inviteByIdEditText.text.toString().lowercase(VectorLocale.applicationLocale).trim() + val text = views.inviteByIdEditText.text.toString().lowercase().trim() if (text.isEmail()) { views.root.hideKeyboard() diff --git a/vector/src/main/java/fr/gouv/tchap/features/roomprofile/settings/linkaccess/TchapRoomLinkAccessFragment.kt b/vector/src/main/java/fr/gouv/tchap/features/roomprofile/settings/linkaccess/TchapRoomLinkAccessFragment.kt index f33422629d..0df9d66783 100644 --- a/vector/src/main/java/fr/gouv/tchap/features/roomprofile/settings/linkaccess/TchapRoomLinkAccessFragment.kt +++ b/vector/src/main/java/fr/gouv/tchap/features/roomprofile/settings/linkaccess/TchapRoomLinkAccessFragment.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import fr.gouv.tchap.features.roomprofile.settings.linkaccess.detail.TchapRoomLinkAccessBottomSheet import fr.gouv.tchap.features.roomprofile.settings.linkaccess.detail.TchapRoomLinkAccessBottomSheetSharedAction import fr.gouv.tchap.features.roomprofile.settings.linkaccess.detail.TchapRoomLinkAccessBottomSheetSharedActionViewModel @@ -40,13 +41,14 @@ import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject -class TchapRoomLinkAccessFragment @Inject constructor( - val viewModelFactory: TchapRoomLinkAccessViewModel.Factory, - val controller: TchapRoomLinkAccessController, - private val avatarRenderer: AvatarRenderer -) : VectorBaseFragment(), +@AndroidEntryPoint +class TchapRoomLinkAccessFragment : VectorBaseFragment(), TchapRoomLinkAccessController.InteractionListener { + @Inject lateinit var viewModelFactory: TchapRoomLinkAccessViewModel.Factory + @Inject lateinit var controller: TchapRoomLinkAccessController + @Inject lateinit var avatarRenderer: AvatarRenderer + private val viewModel: TchapRoomLinkAccessViewModel by fragmentViewModel() private lateinit var sharedActionViewModel: TchapRoomLinkAccessBottomSheetSharedActionViewModel diff --git a/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt b/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt index a665b619c0..d230e76c1e 100644 --- a/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt +++ b/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt @@ -22,6 +22,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.UserProperties +import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository @@ -82,6 +83,13 @@ class SpaceStateHandlerImpl @Inject constructor( return } + analyticsTracker.capture( + ViewRoom( + isDM = false, + isSpace = true, + ) + ) + if (isForwardNavigation) { addToBackstack(spaceToLeave, spaceToSet) } diff --git a/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt b/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt index a75b3fa46b..caa38d20d9 100644 --- a/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ConfigurationModule.kt @@ -44,12 +44,14 @@ object ConfigurationModule { else -> throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}") } return when (config) { - Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "") - is Analytics.PostHog -> AnalyticsConfig( + Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "", "", "") + is Analytics.Enabled -> AnalyticsConfig( isEnabled = true, postHogHost = config.postHogHost, postHogApiKey = config.postHogApiKey, - policyLink = config.policyLink + policyLink = config.policyLink, + sentryDSN = config.sentryDSN, + sentryEnvironment = config.sentryEnvironment ) } } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 8bcfd4e422..38b62e1511 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -88,7 +88,11 @@ import im.vector.app.features.settings.account.deactivation.DeactivateAccountVie import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewModel import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DevicesViewModel +import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreViewModel +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel +import im.vector.app.features.settings.devices.v2.rename.RenameSessionViewModel import im.vector.app.features.settings.devtools.AccountDataViewModel import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel import im.vector.app.features.settings.devtools.KeyRequestListViewModel @@ -641,4 +645,24 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(SessionOverviewViewModel::class) fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(OtherSessionsViewModel::class) + fun otherSessionsViewModelFactory(factory: OtherSessionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SessionDetailsViewModel::class) + fun sessionDetailsViewModelFactory(factory: SessionDetailsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RenameSessionViewModel::class) + fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SessionLearnMoreViewModel::class) + fun sessionLearnMoreViewModelFactory(factory: SessionLearnMoreViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt index d85a51af95..85cd718b4e 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt @@ -25,7 +25,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.util.MatrixItem abstract class BaseProfileMatrixItem(@LayoutRes layoutId: Int) : VectorEpoxyModel(layoutId) { @@ -34,7 +34,7 @@ abstract class BaseProfileMatrixItem(@LayoutRes la @EpoxyAttribute var editable: Boolean = true @EpoxyAttribute - var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null + var userVerificationLevel: UserVerificationLevel? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null @@ -54,6 +54,6 @@ abstract class BaseProfileMatrixItem(@LayoutRes la holder.subtitleView.isVisible = false holder.editableView.isVisible = editable avatarRenderer.render(matrixItem, holder.avatarImageView) - holder.avatarDecorationImageView.render(userEncryptionTrustLevel) + holder.avatarDecorationImageView.renderUser(userVerificationLevel) } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 14e639bf32..2e3e8c9306 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -16,7 +16,6 @@ package im.vector.app.core.extensions -import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import android.net.ConnectivityManager @@ -91,11 +90,9 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? { * * @return true if no active connection is found */ -@Suppress("deprecation") -@SuppressLint("NewApi") // false positive fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean { val connectivityManager = getSystemService()!! - return if (sdkIntProvider.get() > Build.VERSION_CODES.M) { + return if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) when { networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false @@ -104,6 +101,7 @@ fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boo else -> true } } else { + @Suppress("DEPRECATION") when (connectivityManager.activeNetworkInfo?.type) { ConnectivityManager.TYPE_WIFI -> false ConnectivityManager.TYPE_MOBILE -> false 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 64afff4c4d..65013efe1a 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 @@ -16,7 +16,6 @@ package im.vector.app.core.platform -import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.os.Build @@ -85,6 +84,7 @@ import im.vector.app.features.rageshake.RageShake import im.vector.app.features.session.SessionListener import im.vector.app.features.settings.FontScalePreferences import im.vector.app.features.settings.FontScalePreferencesImpl +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ThemeUtils @@ -156,6 +156,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver @Inject lateinit var rageShake: RageShake @Inject lateinit var buildMeta: BuildMeta @Inject lateinit var fontScalePreferences: FontScalePreferences + @Inject lateinit var vectorLocale: VectorLocaleProvider // For debug only @Inject lateinit var debugReceiver: DebugReceiver @@ -177,8 +178,10 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private val restorables = ArrayList() override fun attachBaseContext(base: Context) { - val fontScalePreferences = FontScalePreferencesImpl(PreferenceManager.getDefaultSharedPreferences(base), AndroidSystemSettingsProvider(base)) - val vectorConfiguration = VectorConfiguration(this, fontScalePreferences) + val preferences = PreferenceManager.getDefaultSharedPreferences(base) + val fontScalePreferences = FontScalePreferencesImpl(preferences, AndroidSystemSettingsProvider(base)) + val vectorLocaleProvider = VectorLocaleProvider(preferences) + val vectorConfiguration = VectorConfiguration(this, fontScalePreferences, vectorLocaleProvider) super.attachBaseContext(vectorConfiguration.getLocalisedContext(base)) } @@ -475,23 +478,18 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver /** * Force to render the activity in fullscreen. */ - @Suppress("DEPRECATION") private fun setFullScreen() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } else { - @SuppressLint("WrongConstant") - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE - } + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // New API instead of FLAG_TRANSLUCENT_STATUS window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_NAVIGATION window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) } else { + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index ddc281fdd1..ec6f3288f8 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -25,6 +25,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.CallSuper +import androidx.annotation.FloatRange import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding @@ -39,6 +40,7 @@ import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen +import io.github.hyuwah.draggableviewlib.Utils import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.view.clicks @@ -165,6 +167,13 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe forceExpandState() } + protected fun setPeekHeightAsScreenPercentage(@FloatRange(from = 0.0, to = 1.0) percentage: Float) { + context?.let { + val screenHeight = Utils.getScreenHeight(it) + bottomSheetBehavior?.setPeekHeight((screenHeight * percentage).toInt(), true) + } + } + protected fun forceExpandState() { if (showExpanded) { // Force the bottom sheet to be expanded diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorSharedActionViewModel.kt similarity index 100% rename from vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt rename to vector/src/main/java/im/vector/app/core/platform/VectorSharedActionViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt index d9c6bf3159..0bdfbe8e22 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt @@ -17,16 +17,17 @@ package im.vector.app.core.pushers import android.content.Context +import android.content.SharedPreferences import androidx.core.content.edit -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import javax.inject.Inject class UnifiedPushStore @Inject constructor( val context: Context, - val fcmHelper: FcmHelper + val fcmHelper: FcmHelper, + @DefaultPreferences + private val defaultPrefs: SharedPreferences, ) { - private val defaultPrefs = DefaultSharedPreferences.getInstance(context) - /** * Retrieves the UnifiedPush Endpoint. * diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt index e789585b63..f0a42dd78d 100755 --- a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt @@ -20,11 +20,10 @@ import android.content.Context import android.util.AttributeSet import android.view.View import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.edit import androidx.core.view.isVisible import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.databinding.ViewKeysBackupBannerBinding +import im.vector.app.features.workers.signout.BannerState import timber.log.Timber /** @@ -38,16 +37,12 @@ class KeysBackupBanner @JvmOverloads constructor( ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { var delegate: Delegate? = null - private var state: State = State.Initial + private var state: BannerState = BannerState.Initial private lateinit var views: ViewKeysBackupBannerBinding init { setupView() - DefaultSharedPreferences.getInstance(context).edit { - putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) - putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") - } } /** @@ -56,7 +51,7 @@ class KeysBackupBanner @JvmOverloads constructor( * @param newState the newState representing the view * @param force true to force the rendering of the view */ - fun render(newState: State, force: Boolean = false) { + fun render(newState: BannerState, force: Boolean = false) { if (newState == state && !force) { Timber.v("State unchanged") return @@ -67,48 +62,26 @@ class KeysBackupBanner @JvmOverloads constructor( hideAll() when (newState) { - State.Initial -> renderInitial() - State.Hidden -> renderHidden() - is State.Setup -> renderSetup(newState.numberOfKeys) - is State.Recover -> renderRecover(newState.version) - is State.Update -> renderUpdate(newState.version) - State.BackingUp -> renderBackingUp() + BannerState.Initial -> renderInitial() + BannerState.Hidden -> renderHidden() + is BannerState.Setup -> renderSetup(newState) + is BannerState.Recover -> renderRecover(newState) + is BannerState.Update -> renderUpdate(newState) + BannerState.BackingUp -> renderBackingUp() } } override fun onClick(v: View?) { when (state) { - is State.Setup -> delegate?.setupKeysBackup() - is State.Update, - is State.Recover -> delegate?.recoverKeysBackup() + is BannerState.Setup -> delegate?.setupKeysBackup() + is BannerState.Update, + is BannerState.Recover -> delegate?.recoverKeysBackup() else -> Unit } } private fun onCloseClicked() { - state.let { - when (it) { - is State.Setup -> { - DefaultSharedPreferences.getInstance(context).edit { - putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) - } - } - is State.Recover -> { - DefaultSharedPreferences.getInstance(context).edit { - putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version) - } - } - is State.Update -> { - DefaultSharedPreferences.getInstance(context).edit { - putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version) - } - } - else -> { - // Should not happen, close button is not displayed in other cases - } - } - } - + delegate?.onCloseClicked() // Force refresh render(state, true) } @@ -133,9 +106,8 @@ class KeysBackupBanner @JvmOverloads constructor( isVisible = false } - private fun renderSetup(nbOfKeys: Int) { - if (nbOfKeys == 0 || - DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { + private fun renderSetup(state: BannerState.Setup) { + if (state.numberOfKeys == 0 || state.doNotShowAgain) { // Do not display the setup banner if there is no keys to backup, or if the user has already closed it isVisible = false } else { @@ -148,8 +120,8 @@ class KeysBackupBanner @JvmOverloads constructor( } } - private fun renderRecover(version: String) { - if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) { + private fun renderRecover(state: BannerState.Recover) { + if (state.version == state.doNotShowForVersion) { isVisible = false } else { isVisible = true @@ -161,8 +133,8 @@ class KeysBackupBanner @JvmOverloads constructor( } } - private fun renderUpdate(version: String) { - if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) { + private fun renderUpdate(state: BannerState.Update) { + if (state.version == state.doNotShowForVersion) { isVisible = false } else { isVisible = true @@ -191,61 +163,12 @@ class KeysBackupBanner @JvmOverloads constructor( views.viewKeysBackupBannerLoading.isVisible = false } - /** - * The state representing the view. - * It can take one state at a time. - */ - sealed class State { - // Not yet rendered - object Initial : State() - - // View will be Gone - object Hidden : State() - - // Keys backup is not setup, numberOfKeys is the number of locally stored keys - data class Setup(val numberOfKeys: Int) : State() - - // Keys backup can be recovered, with version from the server - data class Recover(val version: String) : State() - - // Keys backup can be updated - data class Update(val version: String) : State() - - // Keys are backing up - object BackingUp : State() - } - /** * An interface to delegate some actions to another object. */ interface Delegate { + fun onCloseClicked() fun setupKeysBackup() fun recoverKeysBackup() } - - companion object { - /** - * Preference key for setup. Value is a boolean. - */ - private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN" - - /** - * Preference key for recover. Value is a backup version (String). - */ - private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION" - - /** - * Preference key for update. Value is a backup version (String). - */ - private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION" - - /** - * Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version. - */ - fun onRecoverDoneForVersion(context: Context, version: String) { - DefaultSharedPreferences.getInstance(context).edit { - putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version) - } - } - } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index e66306957a..435776f675 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -22,7 +22,9 @@ import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel class ShieldImageView @JvmOverloads constructor( context: Context, @@ -71,6 +73,68 @@ class ShieldImageView @JvmOverloads constructor( null -> Unit } } + + fun renderE2EDecoration(decoration: E2EDecoration?) { + isVisible = true + when (decoration) { + E2EDecoration.WARN_IN_CLEAR -> { + contentDescription = context.getString(R.string.unencrypted) + setImageResource(R.drawable.ic_shield_warning) + } + E2EDecoration.WARN_SENT_BY_UNVERIFIED -> { + contentDescription = context.getString(R.string.encrypted_unverified) + setImageResource(R.drawable.ic_shield_warning) + } + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + contentDescription = context.getString(R.string.encrypted_unverified) + setImageResource(R.drawable.ic_shield_warning) + } + E2EDecoration.WARN_SENT_BY_DELETED_SESSION -> { + contentDescription = context.getString(R.string.encrypted_unverified) + setImageResource(R.drawable.ic_shield_warning) + } + E2EDecoration.WARN_UNSAFE_KEY -> { + contentDescription = context.getString(R.string.key_authenticity_not_guaranteed) + setImageResource( + R.drawable.ic_shield_gray + ) + } + E2EDecoration.NONE, + null -> { + contentDescription = null + isVisible = false + } + } + } + + fun renderUser(userVerificationLevel: UserVerificationLevel?, borderLess: Boolean = false) { + isVisible = userVerificationLevel != null + when (userVerificationLevel) { + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED -> { + contentDescription = context.getString(R.string.a11y_trust_level_trusted) + setImageResource( + if (borderLess) R.drawable.ic_shield_trusted_no_border + else R.drawable.ic_shield_trusted + ) + } + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY, + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED -> { + contentDescription = context.getString(R.string.a11y_trust_level_warning) + setImageResource( + if (borderLess) R.drawable.ic_shield_warning_no_border + else R.drawable.ic_shield_warning + ) + } + UserVerificationLevel.WAS_NEVER_VERIFIED -> { + contentDescription = context.getString(R.string.a11y_trust_level_default) + setImageResource( + if (borderLess) R.drawable.ic_shield_black_no_border + else R.drawable.ic_shield_black + ) + } + null -> Unit + } + } } @DrawableRes diff --git a/vector/src/main/java/im/vector/app/core/utils/CopyToClipboardUseCase.kt b/vector/src/main/java/im/vector/app/core/utils/CopyToClipboardUseCase.kt new file mode 100644 index 0000000000..19ad9e2bba --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/CopyToClipboardUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class CopyToClipboardUseCase @Inject constructor( + @ApplicationContext private val context: Context, +) { + + fun execute(text: CharSequence) { + context.getSystemService() + ?.setPrimaryClip(ClipData.newPlainText("", text)) + } +} 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 74bca41ced..3300e267cc 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 @@ -321,7 +321,6 @@ suspend fun saveMedia( } } -@Suppress("DEPRECATION") private fun saveMediaLegacy( context: Context, mediaMimeType: String?, @@ -352,6 +351,7 @@ private fun saveMediaLegacy( val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename, currentTimeMillis) if (savedFile != null) { val downloadManager = context.getSystemService() + @Suppress("DEPRECATION") downloadManager?.addCompletedDownload( savedFile.name, title, @@ -442,7 +442,6 @@ fun selectTxtFileToWrite( * @param currentTimeMillis the current time in milliseconds * @return the created file */ -@Suppress("DEPRECATION") fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?, currentTimeMillis: Long): File? { // defines another name for the external media var dstFileName: String diff --git a/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt b/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt new file mode 100644 index 0000000000..25901cdf95 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/FirstItemUpdatedObserver.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * This observer detects when item was added or moved to the first position of the adapter, while recyclerView is scrolled to the top. This is necessary + * to force recycler to scroll to the top to make such item visible, because by default it will keep items on screen, while adding new item to the top, + * outside of the viewport + * @param layoutManager - [LinearLayoutManager] of the recycler view, which displays items + * @property onItemUpdated - callback to be called, when observer detects event + */ +class FirstItemUpdatedObserver( + layoutManager: LinearLayoutManager, + private val onItemUpdated: () -> Unit +) : RecyclerView.AdapterDataObserver() { + + val layoutManager: LinearLayoutManager? by weak(layoutManager) + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + if ((toPosition == 0 || fromPosition == 0) && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) { + onItemUpdated.invoke() + } + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0 && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) { + onItemUpdated.invoke() + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 9ad95d3c55..a287626671 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -42,6 +42,7 @@ val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) +val PERMISSIONS_FOR_VOICE_BROADCAST = listOf(Manifest.permission.RECORD_AUDIO) // This is not ideal to store the value like that, but it works private var permissionDialogDisplayed = false diff --git a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt index bbed2f6000..915d840637 100644 --- a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt @@ -17,103 +17,109 @@ package im.vector.app.core.utils import android.content.Context +import android.content.SharedPreferences import android.media.Ringtone import android.media.RingtoneManager import android.net.Uri import androidx.core.content.edit -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject /** - * This file manages the sound ringtone for calls. - * It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices + * This class manages the sound ringtone for calls. + * It allows you to use the default Element Ringtone, or the standard ringtone or set a different one from the available choices * in Android. */ +class RingtoneUtils @Inject constructor( + @DefaultPreferences + private val sharedPreferences: SharedPreferences, + private val context: Context, +) { + /** + * Returns a Uri object that points to a specific Ringtone. + * + * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system + * ringtone for calls. + * + * @return the [Uri] of the currently set [Ringtone] + * @see Ringtone + */ + fun getCallRingtoneUri(): Uri? { + val callRingtone: String? = sharedPreferences + .getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) -/** - * Returns a Uri object that points to a specific Ringtone. - * - * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system - * ringtone for calls. - * - * @return the [Uri] of the currently set [Ringtone] - * @see Ringtone - */ -fun getCallRingtoneUri(context: Context): Uri? { - val callRingtone: String? = DefaultSharedPreferences.getInstance(context) - .getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) + callRingtone?.let { + return Uri.parse(it) + } - callRingtone?.let { - return Uri.parse(it) + return try { + // Use current system notification sound for incoming calls per default (note that it can return null) + RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) + } catch (e: SecurityException) { + // Ignore for now + null + } } - return try { - // Use current system notification sound for incoming calls per default (note that it can return null) - RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) - } catch (e: SecurityException) { - // Ignore for now - null - } -} + /** + * Returns a Ringtone object that can then be played. + * + * If no Ringtone was explicitly set using Riot, it will return the current system ringtone + * for calls. + * + * @return the currently set [Ringtone] + * @see Ringtone + */ + fun getCallRingtone(): Ringtone? { + getCallRingtoneUri()?.let { + // Note that it can also return null + return RingtoneManager.getRingtone(context, it) + } -/** - * Returns a Ringtone object that can then be played. - * - * If no Ringtone was explicitly set using Riot, it will return the current system ringtone - * for calls. - * - * @return the currently set [Ringtone] - * @see Ringtone - */ -fun getCallRingtone(context: Context): Ringtone? { - getCallRingtoneUri(context)?.let { - // Note that it can also return null - return RingtoneManager.getRingtone(context, it) + return null } - return null -} - -/** - * Returns a String with the name of the current Ringtone. - * - * If no Ringtone was explicitly set using Riot, it will return the name of the current system - * ringtone for calls. - * - * @return the name of the currently set [Ringtone], or null - * @see Ringtone - */ -fun getCallRingtoneName(context: Context): String? { - return getCallRingtone(context)?.getTitle(context) -} + /** + * Returns a String with the name of the current Ringtone. + * + * If no Ringtone was explicitly set using Riot, it will return the name of the current system + * ringtone for calls. + * + * @return the name of the currently set [Ringtone], or null + * @see Ringtone + */ + fun getCallRingtoneName(): String? { + return getCallRingtone()?.getTitle(context) + } -/** - * Sets the selected ringtone for riot calls. - * - * @param context Android context - * @param ringtoneUri - * @see Ringtone - */ -fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) { - DefaultSharedPreferences.getInstance(context) - .edit { - putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) - } -} + /** + * Sets the selected ringtone for riot calls. + * + * @param ringtoneUri + * @see Ringtone + */ + fun setCallRingtoneUri(ringtoneUri: Uri) { + sharedPreferences + .edit { + putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) + } + } -/** - * Set using Riot default ringtone. - */ -fun useRiotDefaultRingtone(context: Context): Boolean { - return DefaultSharedPreferences.getInstance(context).getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) -} + /** + * Set using Riot default ringtone. + */ + fun useRiotDefaultRingtone(): Boolean { + return sharedPreferences.getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) + } -/** - * Ask if default Riot ringtone has to be used. - */ -fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) { - DefaultSharedPreferences.getInstance(context) - .edit { - putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) - } + /** + * Ask if default Riot ringtone has to be used. + */ + fun setUseRiotDefaultRingtone(useRiotDefault: Boolean) { + sharedPreferences + .edit { + putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) + } + } } 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 6cfe8acc16..cde4fe2a35 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 @@ -19,8 +19,6 @@ package im.vector.app.core.utils import android.annotation.TargetApi import android.app.Activity import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -100,8 +98,7 @@ fun requestDisablingBatteryOptimization(activity: Activity, activityResultLaunch * @param toastMessage content of the toast message as a String resource */ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage: Int = R.string.copied_to_clipboard) { - val clipboard = context.getSystemService()!! - clipboard.setPrimaryClip(ClipData.newPlainText("", text)) + CopyToClipboardUseCase(context).execute(text) if (showToast) { context.toast(toastMessage) } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 09c6ef9b70..dd2530252e 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -43,9 +43,10 @@ import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.ShortcutsHandler import im.vector.app.features.notifications.NotificationDrawerManager -import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.UnlockedActivity +import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository +import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.session.VectorSessionStore import im.vector.app.features.settings.VectorPreferences @@ -136,10 +137,11 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var uiStateRepository: UiStateRepository @Inject lateinit var shortcutsHandler: ShortcutsHandler - @Inject lateinit var pinCodeStore: PinCodeStore + @Inject lateinit var pinCodeHelper: PinCodeHelper @Inject lateinit var pinLocker: PinLocker @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var vectorAnalytics: VectorAnalytics + @Inject lateinit var lockScreenKeyRepository: LockScreenKeyRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -299,9 +301,10 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity vectorPreferences.clearPreferences() uiStateRepository.reset() pinLocker.unlock() - pinCodeStore.deletePinCode() + pinCodeHelper.deletePinCode() vectorAnalytics.onSignOut() vectorSessionStore.clear() + lockScreenKeyRepository.deleteSystemKey() } withContext(Dispatchers.IO) { // On BG thread 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 449bc05c17..93f3d549cb 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -33,7 +33,6 @@ interface VectorFeatures { fun isScreenSharingEnabled(): Boolean fun isLocationSharingEnabled(): Boolean fun forceUsageOfOpusEncoder(): Boolean - fun shouldStartDmOnFirstMessage(): Boolean /** * This is only to enable if the labs flag should be visible and effective. @@ -42,6 +41,7 @@ interface VectorFeatures { */ fun isNewAppLayoutFeatureEnabled(): Boolean fun isNewDeviceManagementEnabled(): Boolean + fun isVoiceBroadcastEnabled(): Boolean } class DefaultVectorFeatures : VectorFeatures { @@ -56,7 +56,7 @@ class DefaultVectorFeatures : VectorFeatures { override fun isScreenSharingEnabled(): Boolean = true override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING override fun forceUsageOfOpusEncoder(): Boolean = false - override fun shouldStartDmOnFirstMessage(): Boolean = false override fun isNewAppLayoutFeatureEnabled(): Boolean = true override fun isNewDeviceManagementEnabled(): Boolean = false + override fun isVoiceBroadcastEnabled(): Boolean = false } diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt index bffba6fa9c..cc3eed306d 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt @@ -21,4 +21,6 @@ data class AnalyticsConfig( val postHogHost: String, val postHogApiKey: String, val policyLink: String, + val sentryDSN: String, + val sentryEnvironment: String ) diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/UserPropertiesExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/UserPropertiesExt.kt index e5446f438b..0ff04f0854 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/extensions/UserPropertiesExt.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/UserPropertiesExt.kt @@ -17,6 +17,7 @@ package im.vector.app.features.analytics.extensions import im.vector.app.features.analytics.plan.UserProperties +import im.vector.app.features.home.room.list.home.header.HomeRoomFilter import im.vector.app.features.onboarding.FtueUseCase fun FtueUseCase.toTrackingValue(): UserProperties.FtueUseCaseSelection { @@ -27,3 +28,12 @@ fun FtueUseCase.toTrackingValue(): UserProperties.FtueUseCaseSelection { FtueUseCase.SKIP -> UserProperties.FtueUseCaseSelection.Skip } } + +fun HomeRoomFilter.toTrackingValue(): UserProperties.AllChatsActiveFilter { + return when (this) { + HomeRoomFilter.ALL -> UserProperties.AllChatsActiveFilter.All + HomeRoomFilter.UNREADS -> UserProperties.AllChatsActiveFilter.Unreads + HomeRoomFilter.FAVOURITES -> UserProperties.AllChatsActiveFilter.Favourites + HomeRoomFilter.PEOPlE -> UserProperties.AllChatsActiveFilter.People + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index be847dcb7f..553d699d86 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -41,6 +41,7 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( postHogFactory: PostHogFactory, + private val sentryFactory: SentryFactory, analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, @@ -94,6 +95,9 @@ class DefaultVectorAnalytics @Inject constructor( override suspend fun onSignOut() { // reset the analyticsId setAnalyticsId("") + + // Close Sentry SDK. + sentryFactory.stopSentry() } private fun observeAnalyticsId() { @@ -123,10 +127,20 @@ class DefaultVectorAnalytics @Inject constructor( Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent optOutPostHog() + initOrStopSentry() } .launchIn(globalScope) } + private fun initOrStopSentry() { + userConsent?.let { + when (it) { + true -> sentryFactory.initSentry() + false -> sentryFactory.stopSentry() + } + } + } + private fun optOutPostHog() { userConsent?.let { posthog?.optOut(!it) } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt new file mode 100644 index 0000000000..a000f2a77a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.impl + +import android.content.Context +import im.vector.app.features.analytics.AnalyticsConfig +import im.vector.app.features.analytics.log.analyticsTag +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroid +import timber.log.Timber +import javax.inject.Inject + +class SentryFactory @Inject constructor( + private val context: Context, + private val analyticsConfig: AnalyticsConfig, +) { + + fun initSentry() { + Timber.tag(analyticsTag.value).d("Initializing Sentry") + if (Sentry.isEnabled()) return + SentryAndroid.init(context) { options -> + options.dsn = analyticsConfig.sentryDSN + options.beforeSend = SentryOptions.BeforeSendCallback { event, _ -> event } + options.tracesSampleRate = 1.0 + options.isEnableUserInteractionTracing = true + options.environment = analyticsConfig.sentryEnvironment + options.diagnosticLevel + } + } + + fun stopSentry() { + Timber.tag(analyticsTag.value).d("Stopping Sentry") + Sentry.close() + } +} 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 c85c3aa6b5..8536b765d4 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 @@ -40,6 +40,7 @@ import im.vector.app.core.utils.PERMISSIONS_EMPTY import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import kotlin.math.max @@ -75,6 +76,7 @@ class AttachmentTypeSelectorView( views.attachmentContactButton.configure(Type.CONTACT) views.attachmentPollButton.configure(Type.POLL) views.attachmentLocationButton.configure(Type.LOCATION) + views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST) width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -134,6 +136,7 @@ class AttachmentTypeSelectorView( Type.CONTACT -> views.attachmentContactButton Type.POLL -> views.attachmentPollButton Type.LOCATION -> views.attachmentLocationButton + Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast }.let { it.isVisible = isVisible } @@ -221,6 +224,7 @@ class AttachmentTypeSelectorView( STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), - LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location) + LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location), + VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast), } } 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 47b19a435e..20b155d11e 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 @@ -169,11 +169,11 @@ class AttachmentsPreviewFragment : ) } - @Suppress("DEPRECATION") private fun applyInsets() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { activity?.window?.setDecorFitsSystemWindows(false) } else { + @Suppress("DEPRECATION") view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION } ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets -> diff --git a/vector/src/main/java/im/vector/app/features/auth/PendingAuthHandler.kt b/vector/src/main/java/im/vector/app/features/auth/PendingAuthHandler.kt new file mode 100644 index 0000000000..28a6f4b256 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/PendingAuthHandler.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.auth + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.api.session.uia.exceptions.UiaCancelledException +import org.matrix.android.sdk.api.util.fromBase64 +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class PendingAuthHandler @Inject constructor( + private val matrix: Matrix, + private val activeSessionHolder: ActiveSessionHolder, +) { + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + + fun ssoAuthDone() { + pendingAuth?.let { + Timber.d("ssoAuthDone, resuming action") + uiaContinuation?.resume(it) + } ?: run { + Timber.d("ssoAuthDone, cannot resume: no pendingAuth") + uiaContinuation?.resumeWithException(IllegalArgumentException()) + } + } + + fun passwordAuthDone(password: String) { + Timber.d("passwordAuthDone") + val decryptedPass = matrix.secureStorageService() + .loadSecureSecret( + inputStream = password.fromBase64().inputStream(), + keyAlias = ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS + ) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = activeSessionHolder.getActiveSession().myUserId + ) + ) + } + + fun reAuthCancelled() { + Timber.d("reAuthCancelled") + uiaContinuation?.resumeWithException(UiaCancelledException()) + uiaContinuation = null + pendingAuth = null + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt index 8bf2ce47bd..c157ee42b8 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt @@ -20,12 +20,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetCallDialPadBinding -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider +import javax.inject.Inject +@AndroidEntryPoint class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment() { companion object { @@ -41,6 +44,8 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment() { } } - sectionsPagerAdapter = CallTransferPagerAdapter(this) + sectionsPagerAdapter = CallTransferPagerAdapter(this, vectorLocale) views.callTransferViewPager.adapter = sectionsPagerAdapter TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position -> diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt index 3ec8f61978..cb62213398 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt @@ -22,12 +22,13 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.features.call.dialpad.DialPadFragment -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs class CallTransferPagerAdapter( - private val fragmentActivity: FragmentActivity + private val fragmentActivity: FragmentActivity, + private val vectorLocale: VectorLocaleProvider, ) : FragmentStateAdapter(fragmentActivity) { companion object { @@ -61,7 +62,7 @@ class CallTransferPagerAdapter( arguments = Bundle().apply { putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) putBoolean(DialPadFragment.EXTRA_ENABLE_OK, false) - putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) + putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) } } } diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 240ee3fcd2..cfe4388b48 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -52,6 +52,7 @@ enum class Command( MARKDOWN("/markdown", null, "", R.string.command_description_markdown, false, false), RAINBOW("/rainbow", null, "", R.string.command_description_rainbow, false, true), RAINBOW_EMOTE("/rainbowme", null, "", R.string.command_description_rainbow_emote, false, true), + DEVTOOLS("/devtools", null, "", R.string.command_description_devtools, true, false), CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false, false), SPOILER("/spoiler", null, "", R.string.command_description_spoiler, false, true), SHRUG("/shrug", null, "", R.string.command_description_shrug, false, true), @@ -65,7 +66,8 @@ enum class Command( ADD_TO_SPACE("/addToSpace", null, "spaceId", R.string.command_description_add_to_space, true, false), JOIN_SPACE("/joinSpace", null, "spaceId", R.string.command_description_join_space, true, false), LEAVE_ROOM("/leave", null, "", R.string.command_description_leave_room, true, false), - UPGRADE_ROOM("/upgraderoom", null, "newVersion", R.string.command_description_upgrade_room, true, false); + UPGRADE_ROOM("/upgraderoom", null, "newVersion", R.string.command_description_upgrade_room, true, false), + TABLE_FLIP("/tableflip", null, "", R.string.command_description_table_flip, false, true); /** * Whether this command is available in Tchap. @@ -105,7 +107,9 @@ enum class Command( CREATE_SPACE, ADD_TO_SPACE, JOIN_SPACE, - LEAVE_ROOM -> false + LEAVE_ROOM, + DEVTOOLS, + TABLE_FLIP -> false } val allAliases = arrayOf(command, *aliases.orEmpty()) diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index df514ce89f..a8d27acd5b 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -323,6 +323,13 @@ class CommandParser @Inject constructor() { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } + Command.DEVTOOLS.matches(slashCommand) -> { + if (messageParts.size == 1) { + ParsedCommand.DevTools + } else { + ParsedCommand.ErrorSyntax(Command.DEVTOOLS) + } + } Command.CLEAR_SCALAR_TOKEN.matches(slashCommand) -> { if (messageParts.size == 1) { ParsedCommand.ClearScalarToken @@ -343,6 +350,9 @@ class CommandParser @Inject constructor() { Command.LENNY.matches(slashCommand) -> { ParsedCommand.SendLenny(message) } + Command.TABLE_FLIP.matches(slashCommand) -> { + ParsedCommand.SendTableFlip(message) + } Command.DISCARD_SESSION.matches(slashCommand) -> { if (messageParts.size == 1) { ParsedCommand.DiscardSession diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 648dc4fc39..2db93be00f 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -63,8 +63,10 @@ sealed interface ParsedCommand { data class ChangeAvatarForRoom(val url: String) : ParsedCommand data class SetMarkdown(val enable: Boolean) : ParsedCommand object ClearScalarToken : ParsedCommand + object DevTools : ParsedCommand data class SendSpoiler(val message: String) : ParsedCommand data class SendShrug(val message: CharSequence) : ParsedCommand + data class SendTableFlip(val message: CharSequence) : ParsedCommand data class SendLenny(val message: CharSequence) : ParsedCommand object DiscardSession : ParsedCommand data class ShowUser(val userId: String) : ParsedCommand diff --git a/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt index a3d801e534..191338de5b 100644 --- a/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt @@ -22,7 +22,7 @@ import android.os.Build import android.os.LocaleList import androidx.annotation.RequiresApi import im.vector.app.features.settings.FontScalePreferences -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import java.util.Locale @@ -33,21 +33,22 @@ import javax.inject.Inject */ class VectorConfiguration @Inject constructor( private val context: Context, - private val fontScalePreferences: FontScalePreferences + private val fontScalePreferences: FontScalePreferences, + private val vectorLocale: VectorLocaleProvider, ) { fun onConfigurationChanged() { - if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { + if (Locale.getDefault().toString() != vectorLocale.applicationLocale.toString()) { Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") - Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") - Locale.setDefault(VectorLocale.applicationLocale) + Timber.v("## onConfigurationChanged(): restore the expected value ${vectorLocale.applicationLocale}") + Locale.setDefault(vectorLocale.applicationLocale) } // Night mode may have changed ThemeUtils.init(context) } fun applyToApplicationContext() { - val locale = VectorLocale.applicationLocale + val locale = vectorLocale.applicationLocale val fontScale = fontScalePreferences.getResolvedFontScaleValue() Locale.setDefault(locale) @@ -67,7 +68,7 @@ class VectorConfiguration @Inject constructor( */ fun getLocalisedContext(context: Context): Context { try { - val locale = VectorLocale.applicationLocale + val locale = vectorLocale.applicationLocale // create new configuration passing old configuration from original Context val configuration = Configuration(context.resources.configuration) @@ -107,7 +108,7 @@ class VectorConfiguration @Inject constructor( * @return the local status value */ fun getHash(): String { - return (VectorLocale.applicationLocale.toString() + + return (vectorLocale.applicationLocale.toString() + "_" + fontScalePreferences.getResolvedFontScaleValue().preferenceValue + "_" + ThemeUtils.getApplicationTheme(context)) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index a61b685985..34bb04f355 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -30,11 +30,11 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.userdirectory.PendingSelection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,7 +45,7 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser @@ -58,9 +58,9 @@ class CreateDirectRoomViewModel @AssistedInject constructor( private val directRoomHelper: DirectRoomHelper, private val getPlatformTask: TchapGetPlatformTask, private val rawService: RawService, + private val vectorPreferences: VectorPreferences, val session: Session, val analyticsTracker: AnalyticsTracker, - val vectorFeatures: VectorFeatures ) : VectorViewModel(initialState) { @@ -94,11 +94,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor( _viewEvents.post(CreateDirectRoomViewEvents.DmSelf) } else { // Try to get user from known users and fall back to creating a User object from MXID - val qrInvitee = if (session.getUser(mxid) != null) { - session.getUser(mxid)!! - } else { - User(mxid, null, null) - } + val qrInvitee = session.getUserOrDefault(mxid) tchap.onSubmitInvitees(setOf(PendingSelection.UserPendingSelection(qrInvitee))) } } @@ -146,7 +142,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor( } val result = runCatchingToAsync { - if (vectorFeatures.shouldStartDmOnFirstMessage()) { + if (vectorPreferences.isDeferredDmEnabled()) { session.roomService().createLocalRoom(roomParams) } else { analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt index c2cc13920f..466aca1176 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt @@ -16,11 +16,11 @@ package im.vector.app.features.createdirect -import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService @@ -32,7 +32,7 @@ class DirectRoomHelper @Inject constructor( private val rawService: RawService, private val session: Session, private val analyticsTracker: AnalyticsTracker, - private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, ) { suspend fun ensureDMExists(userId: String): String { @@ -50,7 +50,7 @@ class DirectRoomHelper @Inject constructor( setDirectMessage() enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault } - roomId = if (vectorFeatures.shouldStartDmOnFirstMessage()) { + roomId = if (vectorPreferences.isDeferredDmEnabled()) { session.roomService().createLocalRoom(roomParams) } else { analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt index 3089481255..c6e86f6f6b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -18,6 +18,7 @@ package im.vector.app.features.crypto.keysbackup.restore import android.app.Activity import android.content.Context import android.content.Intent +import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -27,8 +28,9 @@ import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity -import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.features.crypto.quads.SharedSecureStorageActivity +import im.vector.app.features.workers.signout.ServerBackupStatusAction +import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import javax.inject.Inject @@ -46,6 +48,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { override fun getTitleRes() = R.string.title_activity_keys_backup_restore private lateinit var viewModel: KeysBackupRestoreSharedViewModel + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() override fun onBackPressed() { hideWaitingView() @@ -95,7 +98,8 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { } KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { viewModel.keyVersionResult.value?.version?.let { - KeysBackupBanner.onRecoverDoneForVersion(this, it) + // Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version. + serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnRecoverDoneForVersion(it)) } replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index cf92afcc2e..3a6b11715c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -29,9 +29,10 @@ import im.vector.app.R import im.vector.app.core.extensions.hidePassword import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class KeysBackupSetupStep2Fragment : @@ -43,6 +44,8 @@ class KeysBackupSetupStep2Fragment : private val zxcvbn = Zxcvbn() + @Inject lateinit var vectorLocale: VectorLocaleProvider + private fun onPassphraseChanged() { viewModel.passphrase.value = views.keysBackupSetupStep2PassphraseEnterEdittext.text.toString() viewModel.confirmPassphraseError.value = null @@ -78,12 +81,12 @@ class KeysBackupSetupStep2Fragment : views.keysBackupSetupStep2PassphraseStrengthLevel.strength = score if (score in 1..3) { - val warning = strength.feedback?.getWarning(VectorLocale.applicationLocale) + val warning = strength.feedback?.getWarning(vectorLocale.applicationLocale) if (warning != null) { views.keysBackupSetupStep2PassphraseEnterTil.error = warning } - val suggestions = strength.feedback?.getSuggestions(VectorLocale.applicationLocale) + val suggestions = strength.feedback?.getSuggestions(vectorLocale.applicationLocale) if (suggestions != null) { views.keysBackupSetupStep2PassphraseEnterTil.error = suggestions.firstOrNull() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 43cc25f195..78f0bc6284 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -28,12 +28,13 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.editorActionEvents import reactivecircus.flowbinding.android.widget.textChanges +import javax.inject.Inject @AndroidEntryPoint class BootstrapEnterPassphraseFragment : @@ -43,6 +44,8 @@ class BootstrapEnterPassphraseFragment : return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false) } + @Inject lateinit var vectorLocale: VectorLocaleProvider + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -105,8 +108,8 @@ class BootstrapEnterPassphraseFragment : views.ssssPassphraseSecurityProgress.strength = score if (score in 1..3) { val hint = - strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } - ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() + strength.feedback?.getWarning(vectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } + ?: strength.feedback?.getSuggestions(vectorLocale.applicationLocale)?.firstOrNull() if (hint != null && hint != views.ssssPassphraseEnterTil.error.toString()) { views.ssssPassphraseEnterTil.error = hint } 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 658fad9284..bab112cd66 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 @@ -32,14 +32,13 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider -import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.raw.wellknown.SecureBackupMethod import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.secureBackupMethod import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth @@ -57,10 +56,8 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.util.awaitCallback -import org.matrix.android.sdk.api.util.fromBase64 import java.io.OutputStream import kotlin.coroutines.Continuation -import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException class BootstrapSharedViewModel @AssistedInject constructor( @@ -71,7 +68,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( private val rawService: RawService, private val bootstrapTask: BootstrapCrossSigningTask, private val migrationTask: BackupToQuadSMigrationTask, - private val matrix: Matrix, + private val pendingAuthHandler: PendingAuthHandler, ) : VectorViewModel(initialState) { private var doesKeyBackupExist: Boolean = false @@ -85,11 +82,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() -// private var _pendingSession: String? = null - - var uiaContinuation: Continuation? = null - var pendingAuth: UIABaseAuth? = null - init { setState { @@ -272,21 +264,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( is BootstrapActions.DoMigrateWithRecoveryKey -> { startMigrationFlow(state.step, null, action.recoveryKey) } - BootstrapActions.SsoAuthDone -> { - uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: "")) - } - is BootstrapActions.PasswordAuthDone -> { - val decryptedPass = matrix.secureStorageService() - .loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) - uiaContinuation?.resume( - UserPasswordAuth( - session = pendingAuth?.session, - password = decryptedPass, - user = session.myUserId - ) - ) - } + BootstrapActions.SsoAuthDone -> pendingAuthHandler.ssoAuthDone() + is BootstrapActions.PasswordAuthDone -> pendingAuthHandler.passwordAuthDone(action.password) BootstrapActions.ReAuthCancelled -> { + pendingAuthHandler.reAuthCancelled() setState { copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error))) } @@ -402,13 +383,13 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { when (flowResponse.nextUncompletedStage()) { LoginFlowTypes.PASSWORD -> { - pendingAuth = UserPasswordAuth( + pendingAuthHandler.pendingAuth = UserPasswordAuth( // Note that _pendingSession may or may not be null, this is OK, it will be managed by the task session = flowResponse.session, user = session.myUserId, password = null ) - uiaContinuation = promise + pendingAuthHandler.uiaContinuation = promise setState { copy( step = BootstrapStep.AccountReAuth() @@ -417,8 +398,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( _viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode)) } LoginFlowTypes.SSO -> { - pendingAuth = DefaultBaseAuth(flowResponse.session) - uiaContinuation = promise + pendingAuthHandler.pendingAuth = DefaultBaseAuth(flowResponse.session) + pendingAuthHandler.uiaContinuation = promise setState { copy( step = BootstrapStep.AccountReAuth() 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 91bb3fa7f2..3406a86d1e 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 @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificatio 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.getUserOrDefault import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber import javax.inject.Inject @@ -67,8 +68,8 @@ class IncomingVerificationRequestHandler @Inject constructor( when (tx.state) { is VerificationTxState.OnStarted -> { // Add a notification for every incoming request - val user = session?.userService()?.getUser(tx.otherUserId) - val name = user?.toMatrixItem()?.getBestName() ?: tx.otherUserId + val user = session.getUserOrDefault(tx.otherUserId).toMatrixItem() + val name = user.getBestName() val alert = VerificationVectorAlert( uid, context.getString(R.string.sas_incoming_request_notif_title), @@ -86,7 +87,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } ) .apply { - viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get()) + viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId) @@ -131,8 +132,8 @@ class IncomingVerificationRequestHandler @Inject constructor( // XXX this is a bit hard coded :/ popupAlertManager.cancelAlert("review_login") } - val user = session?.userService()?.getUser(pr.otherUserId)?.toMatrixItem() - val name = user?.getBestName() ?: pr.otherUserId + val user = session.getUserOrDefault(pr.otherUserId).toMatrixItem() + val name = user.getBestName() val description = if (name == pr.otherUserId) { name } else { 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 eae868eb26..38b72f2022 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 @@ -152,29 +152,25 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment - state.otherUserMxItem?.let { matrixItem -> - if (state.isMe) { - avatarRenderer.render(matrixItem, views.otherUserAvatarImageView) - if (state.sasTransactionState == VerificationTxState.Verified || - state.qrTransactionState == VerificationTxState.Verified || - state.verifiedFromPrivateKeys) { - views.otherUserShield.render(RoomEncryptionTrustLevel.Trusted) - } else { - views.otherUserShield.render(RoomEncryptionTrustLevel.Warning) - } - views.otherUserNameText.text = getString( - if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session - ) + avatarRenderer.render(state.otherUserMxItem, views.otherUserAvatarImageView) + if (state.isMe) { + if (state.sasTransactionState == VerificationTxState.Verified || + state.qrTransactionState == VerificationTxState.Verified || + state.verifiedFromPrivateKeys) { + views.otherUserShield.render(RoomEncryptionTrustLevel.Trusted) } else { - avatarRenderer.render(matrixItem, views.otherUserAvatarImageView) - - if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) { - views.otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName()) - views.otherUserShield.render(RoomEncryptionTrustLevel.Trusted) - } else { - views.otherUserNameText.text = getString(R.string.verification_verify_user, matrixItem.getBestName()) - views.otherUserShield.render(null) - } + views.otherUserShield.render(RoomEncryptionTrustLevel.Warning) + } + views.otherUserNameText.text = getString( + if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session + ) + } else { + if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) { + views.otherUserNameText.text = getString(R.string.verification_verified_user, state.otherUserMxItem.getBestName()) + views.otherUserShield.render(RoomEncryptionTrustLevel.Trusted) + } else { + views.otherUserNameText.text = getString(R.string.verification_verify_user, state.otherUserMxItem.getBestName()) + views.otherUserShield.render(null) } } @@ -235,7 +231,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment = Uninitialized, val pendingLocalId: String? = null, val sasTransactionState: VerificationTxState? = null, @@ -92,7 +93,8 @@ data class VerificationBottomSheetViewState( otherUserId = args.otherUserId, verificationId = args.verificationId, roomId = args.roomId, - selfVerificationMode = args.selfVerificationMode + selfVerificationMode = args.selfVerificationMode, + otherUserMxItem = MatrixItem.UserItem(args.otherUserId), ) } @@ -126,7 +128,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } - val userItem = session.getUser(initialState.otherUserId) + fetchOtherUserProfile(initialState.otherUserId) var autoReady = false val pr = if (initialState.selfVerificationMode) { @@ -160,7 +162,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( setState { copy( - otherUserMxItem = userItem?.toMatrixItem(), sasTransactionState = sasTx?.state, qrTransactionState = qrTx?.state, transactionId = pr?.transactionId ?: initialState.verificationId, @@ -183,6 +184,28 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } + private fun fetchOtherUserProfile(otherUserId: String) { + session.getUser(otherUserId)?.toMatrixItem()?.let { + setState { + copy( + otherUserMxItem = it + ) + } + } + // Always fetch the latest User data + viewModelScope.launch { + tryOrNull { session.userService().resolveUser(otherUserId) } + ?.toMatrixItem() + ?.let { + setState { + copy( + otherUserMxItem = it + ) + } + } + } + } + override fun onCleared() { session.cryptoService().verificationService().removeListener(this) super.onCleared() @@ -216,12 +239,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( private fun cancelAllPendingVerifications(state: VerificationBottomSheetViewState) { session.cryptoService() - .verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let { + .verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)?.let { session.cryptoService().verificationService().cancelVerificationRequest(it) } session.cryptoService() .verificationService() - .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") + .getExistingTransaction(state.otherUserId, state.transactionId ?: "") ?.cancel(CancelCode.User) } @@ -249,7 +272,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } override fun handle(action: VerificationAction) = withState { state -> - val otherUserId = state.otherUserMxItem?.id ?: return@withState + val otherUserId = state.otherUserId val roomId = state.roomId ?: session.roomService().getExistingDirectRoomWithUser(otherUserId) @@ -514,7 +537,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( override fun transactionUpdated(tx: VerificationTransaction) = withState { state -> if (state.selfVerificationMode && state.transactionId == null) { // is this an incoming with that user - if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) { + if (tx.isIncoming && tx.otherUserId == state.otherUserId) { // Also auto accept incoming if needed! if (tx is IncomingSasVerificationTransaction) { if (tx.uxState == IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { @@ -564,7 +587,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( if (state.selfVerificationMode && state.pendingRequest.invoke() == null && state.transactionId == null) { // is this an incoming with that user - if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) { + if (pr.isIncoming && pr.otherUserId == state.otherUserId) { if (!pr.isReady) { // auto ready in this case, as we are waiting // TODO, can I be here in DM mode? in this case should test if roomID is null? diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt index 1adafe2760..600d5e1be1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt @@ -26,6 +26,7 @@ import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.app.features.displayname.getBestName import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject @@ -60,8 +61,8 @@ class VerificationCancelController @Inject constructor( } } } else { - val otherUserID = state.otherUserMxItem?.id ?: "" - val otherDisplayName = state.otherUserMxItem?.displayName ?: "" + val otherUserID = state.otherUserId + val otherDisplayName = state.otherUserMxItem.getBestName() bottomSheetVerificationNoticeItem { id("notice") notice( 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 45f7908446..9f908d83f6 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 @@ -78,7 +78,7 @@ class VerificationChooseMethodFragment : override fun doVerifyBySas() = withState(sharedViewModel) { state -> sharedViewModel.handle( VerificationAction.StartSASVerification( - state.otherUserMxItem?.id ?: "", + state.otherUserId, state.pendingRequest.invoke()?.transactionId ?: "" ) ) @@ -130,7 +130,7 @@ class VerificationChooseMethodFragment : private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) { state -> sharedViewModel.handle( VerificationAction.RemoteQrCodeScanned( - state.otherUserMxItem?.id ?: "", + state.otherUserId, state.pendingRequest.invoke()?.transactionId ?: "", remoteQrCode ) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt index 98b163f4e3..bf514249d8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt @@ -136,7 +136,7 @@ class VerificationEmojiCodeController @Inject constructor( if (state.isWaitingFromOther) { bottomSheetVerificationWaitingItem { id("waiting") - title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUser?.getBestName() ?: "")) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUser.getBestName())) } } else { bottomSheetVerificationActionItem { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt index 1e1a8d0710..58b5c01923 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt @@ -68,13 +68,13 @@ class VerificationEmojiCodeFragment : } override fun onMatchButtonTapped() = withState(viewModel) { state -> - val otherUserId = state.otherUser?.id ?: return@withState + val otherUserId = state.otherUser.id val txId = state.transactionId ?: return@withState sharedViewModel.handle(VerificationAction.SASMatchAction(otherUserId, txId)) } override fun onDoNotMatchButtonTapped() = withState(viewModel) { state -> - val otherUserId = state.otherUser?.id ?: return@withState + val otherUserId = state.otherUser.id val txId = state.transactionId ?: return@withState sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(otherUserId, txId)) } 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 db9a8fed4a..6761d98a55 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 @@ -40,13 +40,13 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTra 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.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem data class VerificationEmojiCodeViewState( val transactionId: String?, - val otherUser: MatrixItem? = null, + val otherUser: MatrixItem, val supportsEmoji: Boolean = true, val emojiDescription: Async> = Uninitialized, val decimalDescription: Async = Uninitialized, @@ -59,15 +59,13 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor( ) : VectorViewModel(initialState), VerificationService.Listener { init { - withState { state -> - refreshStateFromTx( - session.cryptoService().verificationService() - .getExistingTransaction( - state.otherUser?.id ?: "", state.transactionId - ?: "" - ) as? SasVerificationTransaction - ) - } + refreshStateFromTx( + session.cryptoService().verificationService() + .getExistingTransaction( + otherUserId = initialState.otherUser.id, + tid = initialState.transactionId ?: "" + ) as? SasVerificationTransaction + ) session.cryptoService().verificationService().addListener(this) } @@ -165,10 +163,10 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - override fun initialState(viewModelContext: ViewModelContext): VerificationEmojiCodeViewState? { + override fun initialState(viewModelContext: ViewModelContext): VerificationEmojiCodeViewState { val args = viewModelContext.args() val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() - val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem() + val matrixItem = session.getUserOrDefault(args.otherUserId).toMatrixItem() return VerificationEmojiCodeViewState( transactionId = args.verificationId, diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index 71d64b99bc..639ebac29e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -54,7 +54,7 @@ class VerificationQrScannedByOtherController @Inject constructor( if (state.isMe) { notice(host.stringProvider.getString(R.string.qr_code_scanned_self_verif_notice).toEpoxyCharSequence()) } else { - val name = state.otherUserMxItem?.getBestName() ?: "" + val name = state.otherUserMxItem.getBestName() notice(host.stringProvider.getString(R.string.qr_code_scanned_by_other_notice, name).toEpoxyCharSequence()) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index 26ed4b40cc..0a2c085f7e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -53,7 +53,6 @@ class VerificationRequestController @Inject constructor( override fun buildModels() { val state = viewState ?: return - val matrixItem = viewState?.otherUserMxItem ?: return val host = this if (state.selfVerificationMode) { @@ -108,11 +107,9 @@ class VerificationRequestController @Inject constructor( if (state.isMe) { stringProvider.getString(R.string.verify_new_session_notice) } else { - matrixItem.let { - stringProvider.getString(R.string.verification_request_notice, it.id) - .toSpannable() - .colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) - } + stringProvider.getString(R.string.verification_request_notice, state.otherUserId) + .toSpannable() + .colorizeMatchingText(state.otherUserId, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) } bottomSheetVerificationNoticeItem { @@ -139,7 +136,7 @@ class VerificationRequestController @Inject constructor( is Loading -> { bottomSheetVerificationWaitingItem { id("waiting") - title(host.stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUserMxItem.getBestName())) } } is Success -> { @@ -152,7 +149,7 @@ class VerificationRequestController @Inject constructor( } else { bottomSheetVerificationWaitingItem { id("waiting") - title(host.stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUserMxItem.getBestName())) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt index 6887451a76..a466759eae 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt @@ -64,9 +64,7 @@ class VerificationRequestFragment : } override fun onClickOnVerificationStart(): Unit = withState(viewModel) { state -> - state.otherUserMxItem?.id?.let { otherUserId -> - viewModel.handle(VerificationAction.RequestVerificationByDM(otherUserId, state.roomId)) - } + viewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId, state.roomId)) } override fun onClickRecoverFromPassphrase() { diff --git a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt index d92be56267..f24301e564 100644 --- a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt +++ b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt @@ -17,42 +17,49 @@ package im.vector.app.features.disclaimer import android.app.Activity -import android.content.Context +import android.content.SharedPreferences import androidx.core.content.edit import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.features.settings.VectorSettingsUrls +import javax.inject.Inject // Increase this value to show again the disclaimer dialog after an upgrade of the application private const val CURRENT_DISCLAIMER_VALUE = 2 const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE" -fun shouldShowDisclaimerDialog(activity: Activity): Boolean { - val sharedPrefs = DefaultSharedPreferences.getInstance(activity) - return sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE -} - -fun showDisclaimerDialog(activity: Activity) { - // Tchap: condition to show the disclaimer dialog is done at the activity level (see #shouldShowDisclaimerDialog usages) - val sharedPrefs = DefaultSharedPreferences.getInstance(activity) - sharedPrefs.edit { - putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) +class DisclaimerDialog @Inject constructor( + @DefaultPreferences + private val sharedPrefs: SharedPreferences, +) { + // Tchap: Check if disclaimer needs to be displayed + fun shouldShowDisclaimerDialog(): Boolean { + return sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE } - val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_disclaimer_content, null) + fun showDisclaimerDialog(activity: Activity) { + sharedPrefs.edit { + putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) + } - MaterialAlertDialogBuilder(activity) - .setView(dialogLayout) - .setCancelable(false) - .setNeutralButton(R.string.ok, null) - .show() -} + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_disclaimer_content, null) -fun doNotShowDisclaimerDialog(context: Context) { - val sharedPrefs = DefaultSharedPreferences.getInstance(context) + MaterialAlertDialogBuilder(activity) + .setView(dialogLayout) + .setCancelable(false) + .setNegativeButton(R.string.disclaimer_negative_button, null) + .setPositiveButton(R.string.disclaimer_positive_button) { _, _ -> + openUrlInChromeCustomTab(activity, null, VectorSettingsUrls.DISCLAIMER_URL) + } + .show() + } - sharedPrefs.edit { - putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) + fun doNotShowDisclaimerDialog() { + sharedPrefs.edit { + putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) + } } } 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 c6fbf89296..d375d6510e 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 @@ -56,8 +56,7 @@ import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewMode import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.crypto.recover.SetupMode -import im.vector.app.features.disclaimer.shouldShowDisclaimerDialog -import im.vector.app.features.disclaimer.showDisclaimerDialog +import im.vector.app.features.disclaimer.DisclaimerDialog import im.vector.app.features.home.room.list.actions.RoomListSharedAction import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment @@ -86,6 +85,7 @@ import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.webview.VectorWebViewActivity import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import kotlinx.coroutines.flow.launchIn @@ -144,6 +144,7 @@ class HomeActivity : @Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var nightlyProxy: NightlyProxy + @Inject lateinit var disclaimerDialog: DisclaimerDialog private var isNewAppLayoutEnabled: Boolean = false // delete once old app layout is removed @@ -530,7 +531,7 @@ class HomeActivity : ) } - private fun promptSecurityEvent(userItem: MatrixItem.UserItem?, titleRes: Int, descRes: Int, action: ((VectorBaseActivity<*>) -> Unit)) { + private fun promptSecurityEvent(userItem: MatrixItem.UserItem, titleRes: Int, descRes: Int, action: ((VectorBaseActivity<*>) -> Unit)) { popupAlertManager.postVectorAlert( VerificationVectorAlert( uid = "upgradeSecurity", @@ -584,8 +585,8 @@ class HomeActivity : .setPositiveButton(R.string.yes) { _, _ -> bugReporter.openBugReportScreen(this) } .setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile() } .show() - } else if (shouldShowDisclaimerDialog(this)) { - showDisclaimerDialog(this) + } else if (disclaimerDialog.shouldShowDisclaimerDialog()) { + disclaimerDialog.showDisclaimerDialog(this) homeActivityViewModel.handle(HomeActivityViewActions.DisclaimerDialogShown) } @@ -652,10 +653,18 @@ class HomeActivity : launchInviteFriends() true } + R.id.menu_home_qr -> { + launchQrCode() + true + } else -> false } } + private fun launchQrCode() { + startActivity(UserCodeActivity.newIntent(this, sharedActionViewModel.session.myUserId)) + } + private fun launchInviteFriends() { activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends)) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index e0b9e8ceb5..4147cf7186 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -20,13 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.util.MatrixItem sealed interface HomeActivityViewEvents : VectorViewEvents { - data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents + data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class CurrentSessionNotVerified( - val userItem: MatrixItem.UserItem?, + val userItem: MatrixItem.UserItem, val waitForIncomingRequest: Boolean = true, ) : HomeActivityViewEvents data class CurrentSessionCannotBeVerified( - val userItem: MatrixItem.UserItem?, + val userItem: MatrixItem.UserItem, ) : HomeActivityViewEvents data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents 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 9429d8ab5c..27e51137f2 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 @@ -61,7 +61,7 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService 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.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams @@ -123,17 +123,19 @@ class HomeActivityViewModel @AssistedInject constructor( } private fun observeReleaseNotes() = withState { state -> - // we don't want to show release notes for new users or after relogin - if (state.authenticationDescription == null && vectorPreferences.isNewAppLayoutEnabled()) { - releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown -> - if (!isAppLayoutOnboardingShown) { - _viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes) + if (vectorPreferences.isNewAppLayoutEnabled()) { + // we don't want to show release notes for new users or after relogin + if (state.authenticationDescription == null) { + releaseNotesPreferencesStore.appLayoutOnboardingShown.onEach { isAppLayoutOnboardingShown -> + if (!isAppLayoutOnboardingShown) { + _viewEvents.post(HomeActivityViewEvents.ShowReleaseNotes) + } + }.launchIn(viewModelScope) + } else { + // we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user) + viewModelScope.launch { + releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) } - }.launchIn(viewModelScope) - } else { - // we assume that users which came from auth flow either have seen updates already (relogin) or don't need them (new user) - viewModelScope.launch { - releaseNotesPreferencesStore.setAppLayoutOnboardingShown(true) } } } @@ -327,10 +329,10 @@ class HomeActivityViewModel @AssistedInject constructor( } else { // cross signing keys have been reset // Trigger a popup to re-verify - // Note: user can be null in case of logout - session.getUser(session.myUserId) - ?.toMatrixItem() - ?.let { user -> + // Note: user can be unknown in case of logout + session.getUserOrDefault(session.myUserId) + .toMatrixItem() + .let { user -> _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) } } @@ -411,7 +413,7 @@ class HomeActivityViewModel @AssistedInject constructor( // New session _viewEvents.post( HomeActivityViewEvents.CurrentSessionNotVerified( - session.getUser(session.myUserId)?.toMatrixItem(), + session.getUserOrDefault(session.myUserId).toMatrixItem(), // Always send request instead of waiting for an incoming as per recent EW changes false ) @@ -419,7 +421,7 @@ class HomeActivityViewModel @AssistedInject constructor( } else { _viewEvents.post( HomeActivityViewEvents.CurrentSessionCannotBeVerified( - session.getUser(session.myUserId)?.toMatrixItem(), + session.getUserOrDefault(session.myUserId).toMatrixItem(), ) ) } @@ -439,7 +441,7 @@ class HomeActivityViewModel @AssistedInject constructor( // Check this is not an SSO account if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) { // Ask password to the user: Upgrade security - _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem())) + _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUserOrDefault(session.myUserId).toMatrixItem())) } // Else (SSO) just ignore for the moment } else { 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 8f03f5c79f..cece3205e4 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 @@ -59,11 +59,12 @@ import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.BannerState +import im.vector.app.features.workers.signout.ServerBackupStatusAction import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -86,6 +87,7 @@ class HomeDetailFragment : @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var spaceStateHandler: SpaceStateHandler + @Inject lateinit var vectorLocale: VectorLocaleProvider private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() @@ -349,13 +351,15 @@ class HomeDetailFragment : } private fun setupKeysBackupBanner() { + serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed) serverBackupStatusViewModel .onEach { when (val banState = it.bannerState.invoke()) { - is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) - BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) - null, - BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) + is BannerState.Setup, + BannerState.BackingUp, + BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false) + null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false) + else -> Unit /* No op? */ } } views.homeKeysBackupBanner.delegate = this @@ -439,7 +443,7 @@ class HomeDetailFragment : arguments = Bundle().apply { putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true) - putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) + putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) } applyCallback() } @@ -462,6 +466,10 @@ class HomeDetailFragment : * KeysBackupBanner Listener * ========================================================================================== */ + override fun onCloseClicked() { + serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed) + } + override fun setupKeysBackup() { navigator.openKeysBackupSetup(requireActivity(), false) } diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 3681ba4c15..66bb9ef876 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -57,6 +57,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.spaces.SpaceListBottomSheet import im.vector.app.features.workers.signout.BannerState +import im.vector.app.features.workers.signout.ServerBackupStatusAction import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -201,13 +202,12 @@ class NewHomeDetailFragment : private fun setupFabs() { showFABs() - views.newLayoutCreateChatButton.setOnClickListener { - newChatBottomSheet.show(requireActivity().supportFragmentManager, NewChatBottomSheet.TAG) + views.newLayoutCreateChatButton.debouncedClicks { + newChatBottomSheet.takeIf { !it.isAdded }?.show(requireActivity().supportFragmentManager, NewChatBottomSheet.TAG) } - views.newLayoutOpenSpacesButton.setOnClickListener { - // Click action for open spaces modal goes here - spaceListBottomSheet.show(requireActivity().supportFragmentManager, SpaceListBottomSheet.TAG) + views.newLayoutOpenSpacesButton.debouncedClicks { + spaceListBottomSheet.takeIf { !it.isAdded }?.show(requireActivity().supportFragmentManager, SpaceListBottomSheet.TAG) } } @@ -301,13 +301,15 @@ class NewHomeDetailFragment : } private fun setupKeysBackupBanner() { + serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed) serverBackupStatusViewModel .onEach { when (val banState = it.bannerState.invoke()) { - is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) - BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) - null, - BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) + is BannerState.Setup, + BannerState.BackingUp, + BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false) + null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false) + else -> Unit /* No op? */ } } views.homeKeysBackupBanner.delegate = this @@ -349,6 +351,10 @@ class NewHomeDetailFragment : * KeysBackupBanner Listener * ========================================================================================== */ + override fun onCloseClicked() { + serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed) + } + override fun setupKeysBackup() { navigator.openKeysBackupSetup(requireActivity(), false) } diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt index 861fdc64b2..e0565debf2 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.pm.ShortcutInfo import android.graphics.Bitmap import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.WorkerThread import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -32,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O private const val adaptiveIconSizeDp = 108 private const val adaptiveIconOuterSidesDp = 18 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 953f3d9c69..21c7b48dcb 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 @@ -21,10 +21,13 @@ import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -40,14 +43,14 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow import timber.log.Timber data class UnknownDevicesState( - val myMatrixItem: MatrixItem.UserItem? = null, + val myMatrixItem: MatrixItem.UserItem, val unknownSessions: Async> = Uninitialized ) : MavericksState @@ -73,7 +76,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( override fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + override fun initialState(viewModelContext: ViewModelContext): UnknownDevicesState { + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() + + return UnknownDevicesState( + myMatrixItem = session.getUserOrDefault(session.myUserId).toMatrixItem() + ) + } + } private val ignoredDeviceList = ArrayList() @@ -119,7 +130,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( .execute { async -> // Timber.v("## Detector trigger passed distinct") copy( - myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(), + myMatrixItem = session.getUserOrDefault(session.myUserId).toMatrixItem(), unknownSessions = async ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index c1e3b58a80..10708d2290 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -79,6 +79,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class ReRequestKeys(val eventId: String) : RoomDetailAction() object SelectStickerAttachment : RoomDetailAction() + object StartVoiceBroadcast : RoomDetailAction() object OpenIntegrationManager : RoomDetailAction() object ManageIntegrations : RoomDetailAction() data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 3af849e965..399d5e0abe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -51,7 +51,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object OpenRoomProfile : RoomDetailViewEvents() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() - object ShowWaitingView : RoomDetailViewEvents() + data class ShowWaitingView(val text: String? = null) : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() data class DownloadFileState( 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 1fdb621c56..e35695a3f4 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 @@ -361,6 +361,8 @@ class TimelineFragment : private var lockSendButton = false private val currentCallsViewPresenter = CurrentCallsViewPresenter() + private val isEmojiKeyboardVisible: Boolean + get() = vectorPreferences.showEmojiKeyboard() private val lazyLoadedViews = RoomDetailLazyLoadedViews() private val emojiPopup: EmojiPopup by lifecycleAwareLazy { @@ -495,7 +497,7 @@ class TimelineFragment : is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference() - RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() + is RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView(it.text) RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) @@ -1126,6 +1128,7 @@ class TimelineFragment : .findViewById(R.id.action_view_icon_image) .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount") + @Suppress("AlwaysShowAction") matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) } @@ -1537,11 +1540,10 @@ class TimelineFragment : observerUserTyping() - if (vectorPreferences.sendMessageWithEnter()) { - // imeOptions="actionSend" only works with single line, so we remove multiline inputType - composerEditText.inputType = composerEditText.inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv() - composerEditText.imeOptions = EditorInfo.IME_ACTION_SEND + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + composerEditText.setUseIncognitoKeyboard(vectorPreferences.useIncognitoKeyboard()) } + composerEditText.setSendMessageWithEnter(vectorPreferences.sendMessageWithEnter()) composerEditText.setOnEditorActionListener { v, actionId, keyEvent -> val imeActionId = actionId and EditorInfo.IME_MASK_ACTION @@ -1582,10 +1584,18 @@ class TimelineFragment : attachmentTypeSelector.setAttachmentVisibility( AttachmentTypeSelectorView.Type.STICKER, isVisible = false ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentTypeSelectorView.Type.VOICE_BROADCAST, + vectorFeatures.isVoiceBroadcastEnabled(), // TODO check user permission + ) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } + override fun onExpandOrCompactChange() { + views.composerLayout.views.composerEmojiButton.isVisible = isEmojiKeyboardVisible + } + override fun onSendMessage(text: CharSequence) { sendTextMessage(text) } @@ -1826,6 +1836,9 @@ class TimelineFragment : dismissLoadingDialog() views.composerLayout.setTextIfDifferent("") when (parsedCommand) { + is ParsedCommand.DevTools -> { + navigator.openDevTools(requireContext(), timelineArgs.roomId) + } is ParsedCommand.SetMarkdown -> { showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) } @@ -2683,6 +2696,7 @@ class TimelineFragment : locationOwnerId = session.myUserId ) } + AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(RoomDetailAction.StartVoiceBroadcast) } } 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 143734b860..c63bafcf68 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 @@ -40,6 +40,7 @@ import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom +import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.JitsiActiveConferenceHolder @@ -79,12 +80,12 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.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.RelationType @@ -101,9 +102,11 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent @@ -186,6 +189,7 @@ class TimelineViewModel @AssistedInject constructor( init { // This method will take care of a null room to update the state. observeRoomSummary() + observeLocalRoomSummary() if (room == null) { timeline = null } else { @@ -453,6 +457,7 @@ class TimelineViewModel @AssistedInject constructor( is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.StartVoiceBroadcast -> handleStartVoiceBroadcast() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action) @@ -594,6 +599,11 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun handleStartVoiceBroadcast() { + // Todo implement start voice broadcast action + Timber.d("Start voice broadcast clicked") + } + private fun handleOpenIntegrationManager() { viewModelScope.launch { val viewEvent = withContext(Dispatchers.Default) { @@ -618,7 +628,7 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { - _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView()) viewModelScope.launch(Dispatchers.IO) { try { val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo) @@ -638,7 +648,7 @@ class TimelineViewModel @AssistedInject constructor( if (isJitsiWidget) { setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } } else { - _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView()) } session.widgetService().destroyRoomWidget(initialState.roomId, widgetId) // local echo @@ -1239,6 +1249,32 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun observeLocalRoomSummary() { + if (room != null && RoomLocalEcho.isLocalEchoId(room.roomId)) { + room.flow().liveLocalRoomSummary() + .unwrap() + .map { it.creationState } + .distinctUntilChanged() + .onEach { creationState -> + when (creationState) { + LocalRoomCreationState.NOT_CREATED -> Unit + LocalRoomCreationState.CREATING -> + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView(stringProvider.getString(R.string.creating_direct_room))) + LocalRoomCreationState.FAILURE -> { + _viewEvents.post(RoomDetailViewEvents.HideWaitingView) + } + LocalRoomCreationState.CREATED -> { + room.localRoomSummary()?.let { + analyticsTracker.capture(CreatedRoom(isDM = it.roomSummary?.isDirect.orFalse())) + _viewEvents.post(RoomDetailViewEvents.OpenRoom(it.replacementRoomId!!, true)) + } + } + } + } + .launchIn(viewModelScope) + } + } + private fun getUnreadState() { if (room == null) return combine( @@ -1330,26 +1366,11 @@ class TimelineViewModel @AssistedInject constructor( } } room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { - onRoomTombstoneUpdated(it) + setState { copy(tombstoneEvent = it) } } } } - private var roomTombstoneHandled = false - private fun onRoomTombstoneUpdated(tombstoneEvent: Event) = withState { state -> - if (roomTombstoneHandled) return@withState - if (state.isLocalRoom()) { - // Local room has been replaced, so navigate to the new room - val roomId = tombstoneEvent.getClearContent()?.toModel() - ?.replacementRoomId - ?: return@withState - _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true)) - roomTombstoneHandled = true - } else { - setState { copy(tombstoneEvent = tombstoneEvent) } - } - } - /** * Navigates to the appropriate event (by paginating the thread timeline until the event is found * in the snapshot. The main reason for this function is to support the /relations api diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index c751053cdf..9e88882866 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -20,10 +20,12 @@ package im.vector.app.features.home.room.detail.composer import android.content.ClipData import android.content.Context import android.net.Uri +import android.os.Build import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection +import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.OnReceiveContentListener import androidx.core.view.ViewCompat @@ -79,6 +81,27 @@ class ComposerEditText @JvmOverloads constructor( return ic } + /** Set whether the keyboard should disable personalized learning. */ + @RequiresApi(Build.VERSION_CODES.O) + fun setUseIncognitoKeyboard(useIncognitoKeyboard: Boolean) { + imeOptions = if (useIncognitoKeyboard) { + imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } else { + imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() + } + } + + /** Set whether enter should send the message or add a new line. */ + fun setSendMessageWithEnter(sendMessageWithEnter: Boolean) { + if (sendMessageWithEnter) { + inputType = inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv() + imeOptions = imeOptions or EditorInfo.IME_ACTION_SEND + } else { + inputType = inputType or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE + imeOptions = imeOptions and EditorInfo.IME_ACTION_SEND.inv() + } + } + init { addTextChangedListener( object : SimpleTextWatcher() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index 1522960cc9..b1b2c87e9c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -46,6 +46,7 @@ class MessageComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() + fun onExpandOrCompactChange() } val views: ComposerLayoutBinding @@ -96,6 +97,7 @@ class MessageComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) + callback?.onExpandOrCompactChange() } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -105,6 +107,7 @@ class MessageComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) + callback?.onExpandOrCompactChange() } fun setTextIfDifferent(text: CharSequence?): Boolean { 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 66a8a59434..ef55d42e02 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 @@ -272,6 +272,10 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.SetUserPowerLevel -> { handleSetUserPowerLevel(parsedCommand) } + is ParsedCommand.DevTools -> { + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } is ParsedCommand.ClearScalarToken -> { // TODO _viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented) @@ -379,6 +383,11 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) popDraft() } + is ParsedCommand.SendTableFlip -> { + sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) + _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) + popDraft() + } is ParsedCommand.SendChatEffect -> { sendChatEffect(parsedCommand) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index d918703f95..5daf82fae6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -143,6 +143,14 @@ class MessageActionsEpoxyController @Inject constructor( drawableStart(R.drawable.ic_shield_warning_small) } } + E2EDecoration.WARN_UNSAFE_KEY -> { + bottomSheetSendStateItem { + id("e2e_unsafe") + showProgress(false) + text(host.stringProvider.getString(R.string.key_authenticity_not_guaranteed)) + drawableStart(R.drawable.ic_shield_gray) + } + } else -> { // nothing } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index c8a3bb8967..ca93c1389e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -83,7 +83,8 @@ class ViewEditHistoryViewModel @AssistedInject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (e: MXCryptoError) { Timber.w("Failed to decrypt event in history") 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 b71994b760..74a294915b 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 @@ -289,6 +289,9 @@ class MessageItemFactory @Inject constructor( .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) + // Tchap: Use for the Antivirus + .elementToDecrypt(messageContent.encryptedFileInfo?.toElementToDecrypt()) + .contentScannerStateTracker(contentScannerStateTracker) } private fun getAudioFileUrl( @@ -347,6 +350,9 @@ class MessageItemFactory @Inject constructor( .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) + // Tchap: Use for the Antivirus + .elementToDecrypt(messageContent.encryptedFileInfo?.toElementToDecrypt()) + .contentScannerStateTracker(contentScannerStateTracker) } private fun buildVerificationRequestMessageItem( @@ -458,12 +464,15 @@ class MessageItemFactory @Inject constructor( maxWidth = maxWidth, allowNonMxcUrls = informationData.sendState.isSending() ) + + val playable = messageContent.mimeType == MimeTypes.Gif + return MessageImageVideoItem_() .attributes(attributes) .leftGuideline(avatarSizeProvider.leftGuideline) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .playable(messageContent.mimeType == MimeTypes.Gif) + .playable(playable) // Tchap: Use for the Antivirus .contentScannerStateTracker(contentScannerStateTracker) .highlighted(highlight) @@ -479,6 +488,10 @@ class MessageItemFactory @Inject constructor( callback?.onImageMessageClicked(messageContent, data, view, emptyList()) } } + }.apply { + if (playable && vectorPreferences.autoplayAnimatedImages()) { + mode(ImageContentRenderer.Mode.ANIMATED_THUMBNAIL) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index c66e139ab5..d2acc1fc17 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.VerificationState 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.getMsgType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isSticker @@ -147,55 +146,82 @@ class MessageInformationDataFactory @Inject constructor( } private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { - return if ( - event.root.sendState == SendState.SYNCED && - roomSummary?.isEncrypted.orFalse() && - // is user verified - session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { - val ts = roomSummary?.encryptionEventTs ?: 0 - val eventTs = event.root.originServerTs ?: 0 - if (event.isEncrypted()) { + if (roomSummary?.isEncrypted != true) { + // No decoration for clear room + // Questionable? what if the event is E2E? + return E2EDecoration.NONE + } + if (event.root.sendState != SendState.SYNCED) { + // we don't display e2e decoration if event not synced back + return E2EDecoration.NONE + } + val userCrossSigningInfo = session.cryptoService() + .crossSigningService() + .getUserCrossSigningKeys(event.root.senderId.orEmpty()) + + if (userCrossSigningInfo?.isTrusted() == true) { + return if (event.isEncrypted()) { // Do not decorate failed to decrypt, or redaction (we lost sender device info) if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { E2EDecoration.NONE } else { - val sendingDevice = event.root.content - .toModel() - ?.deviceId - ?.let { deviceId -> - session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId) + val sendingDevice = event.root.getSenderKey() + ?.let { + session.cryptoService().deviceWithIdentityKey( + it, + event.root.content?.get("algorithm") as? String ?: "" + ) + } + if (event.root.mxDecryptionResult?.isSafe == false) { + E2EDecoration.WARN_UNSAFE_KEY + } else { + when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session + E2EDecoration.WARN_SENT_BY_DELETED_SESSION + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN + } + sendingDevice.trustLevel?.isVerified().orFalse() -> { + E2EDecoration.NONE + } + else -> { + E2EDecoration.WARN_SENT_BY_UNVERIFIED } - when { - sendingDevice == null -> { - // For now do not decorate this with warning - // maybe it's a deleted session - E2EDecoration.NONE - } - sendingDevice.trustLevel == null -> { - E2EDecoration.WARN_SENT_BY_UNKNOWN - } - sendingDevice.trustLevel?.isVerified().orFalse() -> { - E2EDecoration.NONE - } - else -> { - E2EDecoration.WARN_SENT_BY_UNVERIFIED } } } } else { - if (event.root.isStateEvent()) { - // Do not warn for state event, they are always in clear + e2EDecorationForClearEventInE2ERoom(event, roomSummary) + } + } else { + return if (!event.isEncrypted()) { + e2EDecorationForClearEventInE2ERoom(event, roomSummary) + } else if (event.root.mxDecryptionResult != null) { + if (event.root.mxDecryptionResult?.isSafe == true) { E2EDecoration.NONE } else { - // If event is in clear after the room enabled encryption we should warn - if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + E2EDecoration.WARN_UNSAFE_KEY } + } else { + E2EDecoration.NONE } - } else { - E2EDecoration.NONE } } + private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) = + if (event.root.isStateEvent()) { + // Do not warn for state event, they are always in clear + E2EDecoration.NONE + } else { + val ts = roomSummary.encryptionEventTs ?: 0 + val eventTs = event.root.originServerTs ?: 0 + // If event is in clear after the room enabled encryption we should warn + if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } + /** * Tiles type message never show the sender information (like verification request), so we should repeat it for next message * even if same sender. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 5e23f4db16..ab383f04ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -40,7 +40,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.reactions.widget.ReactionButton import im.vector.app.features.themes.ThemeUtils -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.room.send.SendState private const val MAX_REACTIONS_TO_SHOW = 8 @@ -80,17 +79,7 @@ abstract class AbsBaseMessageItem(@LayoutRes layo override fun bind(holder: H) { super.bind(holder) renderReactions(holder, baseAttributes.informationData.reactionsSummary) - when (baseAttributes.informationData.e2eDecoration) { - E2EDecoration.NONE -> { - holder.e2EDecorationView.render(null) - } - E2EDecoration.WARN_IN_CLEAR, - E2EDecoration.WARN_SENT_BY_UNVERIFIED, - E2EDecoration.WARN_SENT_BY_UNKNOWN -> { - holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning) - } - } - + holder.e2EDecorationView.renderE2EDecoration(baseAttributes.informationData.e2eDecoration) holder.view.onClick(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) (holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index 256019a2cb..05bc03f554 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import android.widget.ImageButton import android.widget.SeekBar import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -33,9 +34,12 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.ContentScannerStateTracker import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils +import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt @EpoxyModelClass abstract class MessageAudioItem : AbsMessageItem() { @@ -71,6 +75,12 @@ abstract class MessageAudioItem : AbsMessageItem() { @EpoxyAttribute lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker + @EpoxyAttribute + var contentScannerStateTracker: ContentScannerStateTracker? = null + + @EpoxyAttribute + var elementToDecrypt: ElementToDecrypt? = null + private var isUserSeeking = false override fun bind(holder: Holder) { @@ -82,6 +92,9 @@ abstract class MessageAudioItem : AbsMessageItem() { bindSeekBar(holder) holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } renderStateBasedOnAudioPlayback(holder) + + holder.resetAV() + contentScannerStateTracker?.bind(attributes.informationData.eventId, mxcUrl, elementToDecrypt, holder) } private fun bindUploadState(holder: Holder) { @@ -193,11 +206,12 @@ abstract class MessageAudioItem : AbsMessageItem() { contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) contentDownloadStateTrackerBinder.unbind(mxcUrl) audioMessagePlaybackTracker.untrack(attributes.informationData.eventId) + contentScannerStateTracker?.unBind(attributes.informationData.eventId) } override fun getViewStubId() = STUB_ID - class Holder : AbsMessageItem.Holder(STUB_ID) { + class Holder : AbsMessageItem.Holder(STUB_ID), ScannableHolder { val rootLayout by bind(R.id.messageRootLayout) val mainLayout by bind(R.id.messageMainInnerLayout) val filenameView by bind(R.id.messageFilenameView) @@ -207,6 +221,46 @@ abstract class MessageAudioItem : AbsMessageItem() { val fileSize by bind(R.id.fileSize) val audioPlaybackDuration by bind(R.id.audioPlaybackDuration) val audioSeekBar by bind(R.id.audioSeekBar) + + private val messageFileAvText by bind(R.id.messageFileAvText) + + fun resetAV() { + messageFileAvText.isVisible = false + } + + override fun mediaScanResult(clean: Boolean) { + if (clean) { + messageFileAvText.text = view.context.getText(R.string.antivirus_clean) + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(view.context, R.drawable.ic_av_checked), + null, + null, + null + ) + } else { + // disable click on infected files + audioPlaybackControlButton.setOnClickListener(null) + audioSeekBar.isEnabled = false + audioSeekBar.setOnSeekBarChangeListener(null) + filenameView.onClick(null) + + messageFileAvText.text = span(view.context.getText(R.string.antivirus_infected)) { + textColor = ThemeUtils.getColor(view.context, R.attr.colorError) + } + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawables(null, null, null, null) + filenameView.text = view.context.getString(R.string.tchap_scan_media_untrusted_content_message, filenameView.text) + } + } + + override fun mediaScanInProgress() { + messageFileAvText.text = span(view.context.getText(R.string.antivirus_in_progress)) { + textColor = ThemeUtils.getColor(view.context, R.attr.vctr_notice_secondary) + } + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawables(null, null, null, null) + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 1113dae356..3357cad4e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -108,7 +108,9 @@ enum class E2EDecoration { NONE, WARN_IN_CLEAR, WARN_SENT_BY_UNVERIFIED, - WARN_SENT_BY_UNKNOWN + WARN_SENT_BY_UNKNOWN, + WARN_SENT_BY_DELETED_SESSION, + WARN_UNSAFE_KEY } enum class SendStateDecoration { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index 93e95dd4a5..b21db8e162 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.doOnPreDraw import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -32,10 +33,13 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.ContentScannerStateTracker import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.voice.AudioWaveformView +import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt @EpoxyModelClass abstract class MessageVoiceItem : AbsMessageItem() { @@ -73,6 +77,12 @@ abstract class MessageVoiceItem : AbsMessageItem() { @EpoxyAttribute lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker + @EpoxyAttribute + var contentScannerStateTracker: ContentScannerStateTracker? = null + + @EpoxyAttribute + var elementToDecrypt: ElementToDecrypt? = null + override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.voiceLayout, null) @@ -94,6 +104,9 @@ abstract class MessageVoiceItem : AbsMessageItem() { ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) } holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) + + holder.resetAV() + contentScannerStateTracker?.bind(attributes.informationData.eventId, mxcUrl, elementToDecrypt, holder) } private fun onWaveformViewReady(holder: Holder) { @@ -169,13 +182,54 @@ abstract class MessageVoiceItem : AbsMessageItem() { override fun getViewStubId() = STUB_ID - class Holder : AbsMessageItem.Holder(STUB_ID) { + class Holder : AbsMessageItem.Holder(STUB_ID), ScannableHolder { val voicePlaybackLayout by bind(R.id.voicePlaybackLayout) val voiceLayout by bind(R.id.voiceLayout) val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) val voicePlaybackTime by bind(R.id.voicePlaybackTime) val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) val progressLayout by bind(R.id.messageFileUploadProgressLayout) + + private val messageFileAvText by bind(R.id.messageFileAvText) + + fun resetAV() { + messageFileAvText.isVisible = false + } + + override fun mediaScanResult(clean: Boolean) { + if (clean) { + messageFileAvText.text = view.context.getText(R.string.antivirus_clean) + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(view.context, R.drawable.ic_av_checked), + null, + null, + null + ) + } else { + // disable click on infected files + voicePlaybackWaveform.setOnTouchListener(null) + voicePlaybackWaveform.setOnLongClickListener(null) + voicePlaybackControlButton.setOnClickListener(null) + + voicePlaybackControlButton.setImageResource(R.drawable.ic_cross) + voicePlaybackControlButton.contentDescription = view.context.getString(R.string.tchap_scan_media_error_file_is_infected) + + messageFileAvText.text = span(view.context.getText(R.string.antivirus_infected)) { + textColor = ThemeUtils.getColor(view.context, R.attr.colorError) + } + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawables(null, null, null, null) + } + } + + override fun mediaScanInProgress() { + messageFileAvText.text = span(view.context.getText(R.string.antivirus_in_progress)) { + textColor = ThemeUtils.getColor(view.context, R.attr.vctr_notice_secondary) + } + messageFileAvText.isVisible = true + messageFileAvText.setCompoundDrawables(null, null, null, null) + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index 467e569756..d5a48db268 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -28,7 +28,6 @@ import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel @EpoxyModelClass abstract class NoticeItem : BaseEventItem(R.layout.item_timeline_event_base_noinfo) { @@ -43,16 +42,7 @@ abstract class NoticeItem : BaseEventItem(R.layout.item_timel holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener) - when (attributes.informationData.e2eDecoration) { - E2EDecoration.NONE -> { - holder.e2EDecorationView.render(null) - } - E2EDecoration.WARN_IN_CLEAR, - E2EDecoration.WARN_SENT_BY_UNVERIFIED, - E2EDecoration.WARN_SENT_BY_UNKNOWN -> { - holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning) - } - } + holder.e2EDecorationView.renderE2EDecoration(attributes.informationData.e2eDecoration) } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index f482cb5ae1..a2759a404c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -34,4 +34,5 @@ sealed class RoomListAction : VectorViewModelAction { object CreateDirectChat : RoomListAction() data class CreateRoom(val initialName: String = "") : RoomListAction() data class OpenRoomDirectory(val filter: String = "") : RoomListAction() + object DeleteAllLocalRoom : RoomListAction() } 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 7be532ae48..36ed2fd3b1 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 @@ -159,10 +159,13 @@ class RoomListFragment : (it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms } } - roomListViewModel.onEach(RoomListViewState::localRoomIds) { - // Local rooms should not exist anymore when the room list is shown - roomListViewModel.deleteLocalRooms(it) - } + } + + override fun onStart() { + super.onStart() + + // Local rooms should not exist anymore when the room list is shown + roomListViewModel.handle(RoomListAction.DeleteAllLocalRoom) } private fun refreshCollapseStates() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 92560a344a..a300b9a926 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState -import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams @@ -98,7 +97,6 @@ class RoomListViewModel @AssistedInject constructor( init { observeMembershipChanges() - observeLocalRooms() spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() @@ -126,23 +124,6 @@ class RoomListViewModel @AssistedInject constructor( } } - private fun observeLocalRooms() { - val queryParams = roomSummaryQueryParams { - memberships = listOf(Membership.JOIN) - } - session - .flow() - .liveRoomSummaries(queryParams) - .map { roomSummaries -> - roomSummaries.mapNotNull { summary -> - summary.roomId.takeIf { RoomLocalEcho.isLocalEchoId(it) } - }.toSet() - } - .setOnEach { roomIds -> - copy(localRoomIds = roomIds) - } - } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val roomListSectionBuilder = RoomListSectionBuilder( @@ -177,6 +158,7 @@ class RoomListViewModel @AssistedInject constructor( RoomListAction.CreateDirectChat -> handleCreateDirectChat() is RoomListAction.CreateRoom -> handleCreateRoom(action) is RoomListAction.OpenRoomDirectory -> handleOpenRoomDirectory(action) + RoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms() } } @@ -184,14 +166,6 @@ class RoomListViewModel @AssistedInject constructor( return session.getRoom(roomId)?.stateService()?.isPublic().orFalse() } - fun deleteLocalRooms(roomsIds: Set) { - viewModelScope.launch { - roomsIds.forEach { - session.roomService().deleteLocalRoom(it) - } - } - } - // PRIVATE METHODS ***************************************************************************** private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState { @@ -361,4 +335,16 @@ class RoomListViewModel @AssistedInject constructor( private fun handleOpenRoomDirectory(action: RoomListAction.OpenRoomDirectory) { _viewEvents.post(RoomListViewEvents.OpenRoomDirectory(action.filter)) } + + private fun handleDeleteLocalRooms() { + val localRoomIds = session.roomService() + .getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) }) + .map { it.roomId } + + viewModelScope.launch { + localRoomIds.forEach { + session.roomService().deleteLocalRoom(it) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index 3f46293346..d897225fd6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -31,7 +31,6 @@ data class RoomListViewState( val asyncSuggestedRooms: Async> = Uninitialized, val currentUserName: String? = null, val asyncSelectedSpace: Async = Uninitialized, - val localRoomIds: Set = emptySet() ) : MavericksState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index b3df0564d2..7c4cba837f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -106,7 +106,11 @@ abstract class RoomSummaryItem : VectorEpoxyModel(R.layo var showSelected: Boolean = false // Tchap items - @EpoxyAttribute lateinit var roomType: TchapRoomType + @EpoxyAttribute + lateinit var roomType: TchapRoomType + + @EpoxyAttribute + var useSingleLineForLastEvent: Boolean = false override fun bind(holder: Holder) { super.bind(holder) @@ -134,6 +138,10 @@ abstract class RoomSummaryItem : VectorEpoxyModel(R.layo // Tchap items renderTchapRoomType(holder) + + if (useSingleLineForLastEvent) { + holder.subtitleView.setLines(1) + } } private fun renderDisplayMode(holder: Holder) = when (displayMode) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 4d3499ab90..1816c9ed72 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -56,7 +56,8 @@ class RoomSummaryItemFactory @Inject constructor( roomChangeMembershipStates: Map, selectedRoomIds: Set, displayMode: RoomListDisplayMode, - listener: RoomListListener? + listener: RoomListListener?, + singleLineLastEvent: Boolean = false ): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> { @@ -64,7 +65,7 @@ class RoomSummaryItemFactory @Inject constructor( createInvitationItem(roomSummary, changeMembershipState, listener) } else -> createRoomItem( - roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked } + roomSummary, selectedRoomIds, displayMode, singleLineLastEvent, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked } ) } } @@ -131,8 +132,9 @@ class RoomSummaryItemFactory @Inject constructor( roomSummary: RoomSummary, selectedRoomIds: Set, displayMode: RoomListDisplayMode, + singleLineLastEvent: Boolean, onClick: ((RoomSummary) -> Unit)?, - onLongClick: ((RoomSummary) -> Boolean)? + onLongClick: ((RoomSummary) -> Boolean)?, ): VectorEpoxyModel<*> { val subtitle = getSearchResultSubtitle(roomSummary) val unreadCount = roomSummary.notificationCount @@ -153,7 +155,7 @@ class RoomSummaryItemFactory @Inject constructor( } else { createRoomSummaryItem( roomSummary, displayMode, subtitle, latestEventTime, typingMessage, - latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick + latestFormattedEvent, showHighlighted, showSelected, unreadCount, singleLineLastEvent, onClick, onLongClick ) } } @@ -168,6 +170,7 @@ class RoomSummaryItemFactory @Inject constructor( showHighlighted: Boolean, showSelected: Boolean, unreadCount: Int, + singleLineLastEvent: Boolean, onClick: ((RoomSummary) -> Unit)?, onLongClick: ((RoomSummary) -> Boolean)? ) = RoomSummaryItem_() @@ -190,6 +193,7 @@ class RoomSummaryItemFactory @Inject constructor( .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) .hasDraft(roomSummary.userDrafts.isNotEmpty()) + .useSingleLineForLastEvent(singleLineLastEvent) .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } .itemClickListener { onClick?.invoke(roomSummary) } // Tchap: Used only for Tchap diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt index d4683f78a5..df191bc2ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemPlaceHolder.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.list +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder @@ -23,5 +25,18 @@ import im.vector.app.core.epoxy.VectorEpoxyModel @EpoxyModelClass abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel(R.layout.item_room_placeholder) { - class Holder : VectorEpoxyHolder() + + @EpoxyAttribute + var useSingleLineForLastEvent: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + if (useSingleLineForLastEvent) { + holder.subtitleView.setLines(1) + } + } + + class Holder : VectorEpoxyHolder() { + val subtitleView by bind(R.id.subtitleView) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt index 2eb8921fd5..a2b6ed51d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt @@ -17,18 +17,26 @@ package im.vector.app.features.home.room.list import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryListController( private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val displayMode: RoomListDisplayMode + private val displayMode: RoomListDisplayMode, + fontScalePreferences: FontScalePreferences ) : CollapsableTypedEpoxyController>() { var listener: RoomListListener? = null + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } override fun buildModels(data: List?) { data?.forEach { - add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener)) + add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener, shouldUseSingleLine)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt index 445438eec9..10d7ef425c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt @@ -20,18 +20,26 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.app.core.utils.createUIHandler import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryPagedController( private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val displayMode: RoomListDisplayMode + private val displayMode: RoomListDisplayMode, + fontScalePreferences: FontScalePreferences ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ), CollapsableControllerExtension { var listener: RoomListListener? = null + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } var roomChangeMembershipStates: Map? = null set(value) { @@ -57,8 +65,14 @@ class RoomSummaryPagedController( } override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { - // for place holder if enabled - item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } - return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener) + return if (item == null) { + val host = this + RoomSummaryItemPlaceHolder_().apply { + id(currentPosition) + useSingleLineForLastEvent(host.shouldUseSingleLine) + } + } else { + roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener, shouldUseSingleLine) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt index f72698048d..c5edd9c063 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt @@ -17,18 +17,20 @@ package im.vector.app.features.home.room.list import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.settings.FontScalePreferences import javax.inject.Inject class RoomSummaryPagedControllerFactory @Inject constructor( - private val roomSummaryItemFactory: RoomSummaryItemFactory + private val roomSummaryItemFactory: RoomSummaryItemFactory, + private val fontScalePreferences: FontScalePreferences ) { fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController { - return RoomSummaryPagedController(roomSummaryItemFactory, displayMode) + return RoomSummaryPagedController(roomSummaryItemFactory, displayMode, fontScalePreferences) } fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController { - return RoomSummaryListController(roomSummaryItemFactory, displayMode) + return RoomSummaryListController(roomSummaryItemFactory, displayMode, fontScalePreferences) } fun createSuggestedRoomListController(): SuggestedRoomListController { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt similarity index 59% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt index 789c9e9985..cd245af0fc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt @@ -14,8 +14,9 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list.home.filter +package im.vector.app.features.home.room.list.home +import androidx.paging.PagedList import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.app.core.platform.StateView @@ -24,12 +25,14 @@ import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ -import im.vector.app.features.home.room.list.home.roomListEmptyItem +import im.vector.app.features.settings.FontScalePreferences import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary +import javax.inject.Inject -class HomeFilteredRoomsController( +class HomeFilteredRoomsController @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory, + fontScalePreferences: FontScalePreferences ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() @@ -43,32 +46,32 @@ class HomeFilteredRoomsController( } var listener: RoomListListener? = null - var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null - private var filtersData: List? = null private var emptyStateData: StateView.State.Empty? = null - private var currentState: StateView.State = StateView.State.Content - override fun addModels(models: List>) { - val host = this - if (host.filtersData != null) { - roomFilterHeaderItem { - id("filter_header") - filtersData(host.filtersData) - onFilterChangedListener(host.onFilterChanged) - } + private val shouldUseSingleLine: Boolean + + init { + val fontScale = fontScalePreferences.getResolvedFontScaleValue() + shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE + } + + fun submitRoomsList(roomsList: PagedList) { + submitList(roomsList) + // If room is empty we may have a new EmptyState to display + if (roomsList.isEmpty()) { + requestForcedModelBuild() } + } + override fun addModels(models: List>) { + val emptyStateData = this.emptyStateData if (models.isEmpty() && emptyStateData != null) { - emptyStateData?.let { emptyState -> - roomListEmptyItem { - id("state_item") - emptyData(emptyState) - } - currentState = emptyState + roomListEmptyItem { + id("state_item") + emptyData(emptyStateData) } } else { - currentState = StateView.State.Content super.addModels(models) } } @@ -77,12 +80,22 @@ class HomeFilteredRoomsController( this.emptyStateData = state } - fun submitFiltersData(data: List?) { - this.filtersData = data - requestForcedModelBuild() - } override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { - item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } - return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) + return if (item == null) { + val host = this + RoomSummaryItemPlaceHolder_().apply { + id(currentPosition) + useSingleLineForLastEvent(host.shouldUseSingleLine) + } + } else { + roomSummaryItemFactory.create( + roomSummary = item, + roomChangeMembershipStates = roomChangeMembershipStates.orEmpty(), + selectedRoomIds = emptySet(), + displayMode = RoomListDisplayMode.ROOMS, + listener = listener, + singleLineLastEvent = shouldUseSingleLine + ) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt index 6d17792969..5760874812 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.list.home import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter +import im.vector.app.features.home.room.list.home.header.HomeRoomFilter import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -27,4 +27,5 @@ sealed class HomeRoomListAction : VectorViewModelAction { data class ToggleTag(val roomId: String, val tag: String) : HomeRoomListAction() data class LeaveRoom(val roomId: String) : HomeRoomListAction() data class ChangeRoomFilter(val filter: HomeRoomFilter) : HomeRoomListAction() + object DeleteAllLocalRoom : HomeRoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index edb619cd90..5677f3e4a8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -24,11 +24,8 @@ import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyControllerAdapter import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -37,19 +34,17 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.core.utils.FirstItemUpdatedObserver import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.room.list.RoomListAnimator import im.vector.app.features.home.room.list.RoomListListener -import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel -import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController -import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter +import im.vector.app.features.home.room.list.home.header.HomeRoomFilter +import im.vector.app.features.home.room.list.home.header.HomeRoomsHeadersController import im.vector.app.features.home.room.list.home.invites.InvitesActivity -import im.vector.app.features.home.room.list.home.invites.InvitesCounterController -import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -63,14 +58,14 @@ class HomeRoomListFragment : VectorBaseFragment(), RoomListListener { - @Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory @Inject lateinit var userPreferencesProvider: UserPreferencesProvider - @Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController - @Inject lateinit var invitesCounterController: InvitesCounterController + @Inject lateinit var headersController: HomeRoomsHeadersController + @Inject lateinit var roomsController: HomeFilteredRoomsController private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel() private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel private var concatAdapter = ConcatAdapter() + private lateinit var firstItemObserver: FirstItemUpdatedObserver private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var stateRestorer: LayoutManagerStateRestorer @@ -87,6 +82,13 @@ class HomeRoomListFragment : setupRecyclerView() } + override fun onStart() { + super.onStart() + + // Local rooms should not exist anymore when the room list is shown + roomListViewModel.handle(HomeRoomListAction.DeleteAllLocalRoom) + } + private fun setupObservers() { sharedQuickActionsViewModel = activityViewModelProvider[RoomListQuickActionsSharedActionViewModel::class.java] sharedQuickActionsViewModel @@ -128,14 +130,17 @@ class HomeRoomListFragment : roomListViewModel.handle(HomeRoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY)) } is RoomListQuickActionsSharedAction.Leave -> { - roomListViewModel.handle(HomeRoomListAction.LeaveRoom(quickAction.roomId)) promptLeaveRoom(quickAction.roomId) } } } private fun setupRecyclerView() { + views.stateView.state = StateView.State.Content val layoutManager = LinearLayoutManager(context) + firstItemObserver = FirstItemUpdatedObserver(layoutManager) { + layoutManager.scrollToPosition(0) + } stateRestorer = LayoutManagerStateRestorer(layoutManager).register() views.roomListView.layoutManager = layoutManager views.roomListView.itemAnimator = RoomListAnimator() @@ -143,33 +148,39 @@ class HomeRoomListFragment : modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } - roomListViewModel.sections.onEach { sections -> - setUpAdapters(sections) - }.launchIn(lifecycleScope) + roomListViewModel.onEach(HomeRoomListViewState::headersData) { + headersController.submitData(it) + } + roomListViewModel.roomsLivePagedList.observe(viewLifecycleOwner) { roomsList -> + roomsController.submitRoomsList(roomsList) + } + roomListViewModel.onEach(HomeRoomListViewState::emptyState) { emptyState -> + roomsController.submitEmptyStateData(emptyState) + } + + setUpAdapters() views.roomListView.adapter = concatAdapter - // we need to force scroll when recents/filter tabs are added to make them visible - concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (positionStart == 0) { - layoutManager.scrollToPosition(0) - } - } - }) + concatAdapter.registerAdapterDataObserver(firstItemObserver) } - override fun invalidate() = withState(roomListViewModel) { state -> - views.stateView.state = state.state - } + override fun invalidate() = Unit - private fun setUpAdapters(sections: Set) { - concatAdapter.adapters.forEach { - concatAdapter.removeAdapter(it) - } - sections.forEach { - concatAdapter.addAdapter(getAdapterForData(it)) - } + private fun setUpAdapters() { + val headersAdapter = headersController.also { controller -> + controller.invitesClickListener = ::onInvitesCounterClicked + controller.onFilterChangedListener = ::onRoomFilterChanged + controller.recentsRoomListener = this + }.adapter + + val roomsAdapter = roomsController + .also { controller -> + controller.listener = this + }.adapter + + concatAdapter.addAdapter(headersAdapter) + concatAdapter.addAdapter(roomsAdapter) } private fun promptLeaveRoom(roomId: String) { @@ -191,43 +202,6 @@ class HomeRoomListFragment : .show() } - private fun getAdapterForData(section: HomeRoomSection): EpoxyControllerAdapter { - return when (section) { - is HomeRoomSection.RoomSummaryData -> { - HomeFilteredRoomsController( - roomSummaryItemFactory, - ).also { controller -> - controller.listener = this - controller.onFilterChanged = ::onRoomFilterChanged - roomListViewModel.emptyStateFlow.onEach { emptyStateOptional -> - controller.submitEmptyStateData(emptyStateOptional.getOrNull()) - }.launchIn(lifecycleScope) - section.filtersData.onEach { - controller.submitFiltersData(it.getOrNull()) - }.launchIn(lifecycleScope) - section.list.observe(viewLifecycleOwner) { list -> - controller.submitList(list) - if (list.isEmpty()) { - controller.requestForcedModelBuild() - } - } - }.adapter - } - is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller -> - controller.listener = this - section.list.observe(viewLifecycleOwner) { list -> - controller.submitList(list) - } - }.adapter - is HomeRoomSection.InvitesCountData -> invitesCounterController.also { controller -> - controller.clickListener = ::onInvitesCounterClicked - section.count.observe(viewLifecycleOwner) { count -> - controller.submitData(count) - } - }.adapter - } - } - private fun onInvitesCounterClicked() { startActivity(Intent(activity, InvitesActivity::class.java)) } @@ -247,8 +221,15 @@ class HomeRoomListFragment : override fun onDestroyView() { views.roomListView.cleanup() - recentRoomCarouselController.listener = null - invitesCounterController.clickListener = null + + headersController.recentsRoomListener = null + headersController.invitesClickListener = null + headersController.onFilterChangedListener = null + + roomsController.listener = null + + concatAdapter.unregisterAdapterDataObserver(firstItemObserver) + super.onDestroyView() } @@ -266,21 +247,13 @@ class HomeRoomListFragment : return true } - override fun onRejectRoomInvitation(room: RoomSummary) { - TODO("Not yet implemented") - } + override fun onRejectRoomInvitation(room: RoomSummary) = Unit - override fun onAcceptRoomInvitation(room: RoomSummary) { - TODO("Not yet implemented") - } + override fun onAcceptRoomInvitation(room: RoomSummary) = Unit - override fun onJoinSuggestedRoom(room: SpaceChildInfo) { - TODO("Not yet implemented") - } + override fun onJoinSuggestedRoom(room: SpaceChildInfo) = Unit - override fun onSuggestedRoomClicked(room: SpaceChildInfo) { - TODO("Not yet implemented") - } + override fun onSuggestedRoomClicked(room: SpaceChildInfo) = Unit // endregion } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index b52c4e0190..33b293497e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -17,7 +17,9 @@ package im.vector.app.features.home.room.list.home import android.widget.ImageView -import androidx.lifecycle.map +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.paging.PagedList import arrow.core.toOption import com.airbnb.mvrx.MavericksViewModelFactory @@ -32,15 +34,16 @@ import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toTrackingValue +import im.vector.app.features.analytics.plan.UserProperties +import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.list.home.header.HomeRoomFilter +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -53,15 +56,18 @@ import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult 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.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow class HomeRoomListViewModel @AssistedInject constructor( @@ -71,6 +77,7 @@ class HomeRoomListViewModel @AssistedInject constructor( private val preferencesStore: HomeLayoutPreferencesStore, private val stringProvider: StringProvider, private val drawableProvider: DrawableProvider, + private val analyticsTracker: AnalyticsTracker, ) : VectorViewModel(initialState) { @AssistedFactory @@ -84,96 +91,26 @@ class HomeRoomListViewModel @AssistedInject constructor( .setPageSize(10) .setInitialLoadSizeHint(20) .setEnablePlaceholders(true) - .setPrefetchDistance(10) .build() - private val _sections = MutableSharedFlow>(replay = 1) - val sections = _sections.asSharedFlow() + private val _roomsLivePagedList = MutableLiveData>() + val roomsLivePagedList: LiveData> = _roomsLivePagedList - private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL - private val _emptyStateFlow = MutableSharedFlow>(replay = 1) - val emptyStateFlow = _emptyStateFlow.asSharedFlow() + private val internalPagedListObserver = Observer> { + _roomsLivePagedList.postValue(it) + } private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null init { - configureSections() - observePreferences() - } - - private fun observePreferences() { - preferencesStore.areRecentsEnabledFlow.onEach { - configureSections() - }.launchIn(viewModelScope) - - preferencesStore.isAZOrderingEnabledFlow.onEach { - configureSections() - }.launchIn(viewModelScope) + observeOrderPreferences() + observeInvites() + observeRecents() + observeFilterTabs() + observeSpaceChanges() } - private fun configureSections() = viewModelScope.launch { - val newSections = mutableSetOf() - newSections.add(getInvitesCountSection()) - - val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first() - if (areSettingsEnabled) { - newSections.add(getRecentRoomsSection()) - } - newSections.add(getFilteredRoomsSection()) - - emitEmptyState() - _sections.emit(newSections) - - setState { - copy(state = StateView.State.Content) - } - } - - private fun getRecentRoomsSection(): HomeRoomSection { - val liveList = session.roomService() - .getBreadcrumbsLive(roomSummaryQueryParams { - displayName = QueryStringValue.NoCondition - memberships = listOf(Membership.JOIN) - }) - - return HomeRoomSection.RecentRoomsData( - list = liveList - ) - } - - private fun getInvitesCountSection(): HomeRoomSection.InvitesCountData { - val builder = RoomSummaryQueryParams.Builder().also { - it.memberships = listOf(Membership.INVITE) - } - - val liveCount = session.roomService().getRoomSummariesLive( - builder.build(), - RoomSortOrder.ACTIVITY - ).map { it.count() } - - return HomeRoomSection.InvitesCountData(liveCount) - } - - private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData { - val builder = RoomSummaryQueryParams.Builder().also { - it.memberships = listOf(Membership.JOIN) - } - - val params = getFilteredQueryParams(HomeRoomFilter.ALL, builder.build()) - val sortOrder = if (preferencesStore.isAZOrderingEnabledFlow.first()) { - RoomSortOrder.NAME - } else { - RoomSortOrder.ACTIVITY - } - - val liveResults = session.roomService().getFilteredPagedRoomSummariesLive( - params, - pagedListConfig, - sortOrder - ).also { - this.filteredPagedRoomSummariesLive = it - } - + private fun observeSpaceChanges() { spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() .onStart { @@ -181,34 +118,79 @@ class HomeRoomListViewModel @AssistedInject constructor( } .onEach { selectedSpaceOption -> val selectedSpace = selectedSpaceOption.orNull() - liveResults.queryParams = liveResults.queryParams.copy( - spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter() - ) - emitEmptyState() + updateEmptyState() + filteredPagedRoomSummariesLive?.let { liveResults -> + liveResults.queryParams = liveResults.queryParams.copy( + spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter() + ) + } + } + .launchIn(viewModelScope) + } + + private fun observeInvites() { + session.flow() + .liveRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + }, + RoomSortOrder.ACTIVITY + ).onEach { list -> + setState { copy(headersData = headersData.copy(invitesCount = list.size)) } }.launchIn(viewModelScope) + } - return HomeRoomSection.RoomSummaryData( - list = liveResults.livePagedList, - filtersData = getFiltersDataFlow() - ) + private fun observeRecents() { + preferencesStore.areRecentsEnabledFlow + .distinctUntilChanged() + .flatMapLatest { areEnabled -> + if (areEnabled) { + session.flow() + .liveBreadcrumbs(roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + }) + .map { Optional.from(it) } + } else { + flowOf(Optional.empty()) + }.onEach { listOptional -> + setState { copy(headersData = headersData.copy(recents = listOptional.getOrNull())) } + } + }.launchIn(viewModelScope) } - private fun emitEmptyState() { - viewModelScope.launch { - val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace()) - _emptyStateFlow.emit(Optional.from(emptyState)) - } + private fun observeFilterTabs() { + preferencesStore.areFiltersEnabledFlow + .distinctUntilChanged() + .flatMapLatest { areEnabled -> + getFilterTabsFlow(areEnabled) + }.onEach { filtersOptional -> + val filters = filtersOptional.getOrNull() + if (!isCurrentFilterStillValid(filters)) { + changeRoomFilter(HomeRoomFilter.ALL) + } + setState { + copy( + headersData = headersData.copy( + filtersList = filters, + ) + ) + } + }.launchIn(viewModelScope) } - private fun getFiltersDataFlow(): SharedFlow>> { - val flow = MutableSharedFlow>>(replay = 1) + private suspend fun isCurrentFilterStillValid(filtersList: List?): Boolean { + if (filtersList.isNullOrEmpty()) return false + val currentFilter = awaitState().headersData.currentFilter + return filtersList.contains(currentFilter) + } + private fun getFilterTabsFlow(isEnabled: Boolean): Flow>> { + if (!isEnabled) return flowOf(Optional.empty()) val spaceFLow = spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() .onStart { emit(spaceStateHandler.getCurrentSpace().toOption()) } - val favouritesFlow = spaceFLow.flatMapLatest { selectedSpace -> session.flow() @@ -236,31 +218,61 @@ class HomeRoomListViewModel @AssistedInject constructor( .map { it.isNotEmpty() } .distinctUntilChanged() - combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled -> - Triple(hasFavourite, hasDm, areFiltersEnabled) - }.onEach { (hasFavourite, hasDm, areFiltersEnabled) -> - if (areFiltersEnabled) { - val filtersData = mutableListOf( - HomeRoomFilter.ALL, - HomeRoomFilter.UNREADS + return combine(favouritesFlow, dmsFLow) { hasFavourite, hasDm -> + hasFavourite to hasDm + }.map { (hasFavourite, hasDm) -> + val filtersData = mutableListOf( + HomeRoomFilter.ALL, + HomeRoomFilter.UNREADS + ) + if (hasFavourite) { + filtersData.add( + HomeRoomFilter.FAVOURITES + ) + } + if (hasDm) { + filtersData.add( + HomeRoomFilter.PEOPlE ) - if (hasFavourite) { - filtersData.add( - HomeRoomFilter.FAVOURITES - ) - } - if (hasDm) { - filtersData.add( - HomeRoomFilter.PEOPlE - ) - } - flow.emit(Optional.from(filtersData)) - } else { - flow.emit(Optional.empty()) } - }.launchIn(viewModelScope) + Optional.from(filtersData) + } + } + + private fun observeRooms(currentFilter: HomeRoomFilter, isAZOrdering: Boolean) { + filteredPagedRoomSummariesLive?.livePagedList?.removeObserver(internalPagedListObserver) + val builder = RoomSummaryQueryParams.Builder().also { + it.memberships = listOf(Membership.JOIN) + it.spaceFilter = spaceStateHandler.getCurrentSpace()?.roomId.toActiveSpaceOrNoFilter() + } + val params = getFilteredQueryParams(currentFilter, builder.build()) + val sortOrder = if (isAZOrdering) { + RoomSortOrder.NAME + } else { + RoomSortOrder.ACTIVITY + } + val liveResults = session.roomService().getFilteredPagedRoomSummariesLive( + params, + pagedListConfig, + sortOrder + ).also { + filteredPagedRoomSummariesLive = it + } + liveResults.livePagedList.observeForever(internalPagedListObserver) + } + + private fun observeOrderPreferences() { + preferencesStore.isAZOrderingEnabledFlow + .onEach { isAZOrdering -> + val currentFilter = awaitState().headersData.currentFilter + observeRooms(currentFilter, isAZOrdering) + }.launchIn(viewModelScope) + } - return flow + private suspend fun updateEmptyState() { + val currentFilter = awaitState().headersData.currentFilter + val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace()) + setState { copy(emptyState = emptyState) } } private fun getFilteredQueryParams(filter: HomeRoomFilter, currentParams: RoomSummaryQueryParams): RoomSummaryQueryParams { @@ -296,7 +308,7 @@ class HomeRoomListViewModel @AssistedInject constructor( isBigImage = true ) } else { - val userName = session.userService().getUser(session.myUserId)?.displayName ?: "" + val userName = session.getUserOrDefault(session.myUserId).toMatrixItem().getBestName() StateView.State.Empty( title = stringProvider.getString(R.string.home_empty_no_rooms_title, userName), message = stringProvider.getString(R.string.home_empty_no_rooms_message), @@ -323,17 +335,33 @@ class HomeRoomListViewModel @AssistedInject constructor( is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action) is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is HomeRoomListAction.ToggleTag -> handleToggleTag(action) - is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action) + is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action.filter) + HomeRoomListAction.DeleteAllLocalRoom -> handleDeleteLocalRooms() } } - private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { - currentFilter = action.filter - filteredPagedRoomSummariesLive?.let { liveResults -> - liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) + override fun onCleared() { + filteredPagedRoomSummariesLive?.livePagedList?.removeObserver(internalPagedListObserver) + super.onCleared() + } + + private fun handleChangeRoomFilter(newFilter: HomeRoomFilter) { + viewModelScope.launch { + changeRoomFilter(newFilter) } + } - emitEmptyState() + private suspend fun changeRoomFilter(newFilter: HomeRoomFilter) { + val currentFilter = awaitState().headersData.currentFilter + if (currentFilter == newFilter) { + return + } + setState { copy(headersData = headersData.copy(currentFilter = newFilter)) } + updateEmptyState() + analyticsTracker.updateUserProperties(UserProperties(allChatsActiveFilter = newFilter.toTrackingValue())) + filteredPagedRoomSummariesLive?.let { liveResults -> + liveResults.queryParams = getFilteredQueryParams(newFilter, liveResults.queryParams) + } } fun isPublicRoom(roomId: String): Boolean { @@ -354,9 +382,9 @@ class HomeRoomListViewModel @AssistedInject constructor( } private fun handleChangeNotificationMode(action: HomeRoomListAction.ChangeRoomNotificationState) { - val room = session.getRoom(action.roomId) - if (room != null) { - viewModelScope.launch { + viewModelScope.launch { + val room = session.getRoom(action.roomId) + if (room != null) { try { room.roomPushRuleService().setRoomNotificationState(action.notificationState) } catch (failure: Throwable) { @@ -367,8 +395,8 @@ class HomeRoomListViewModel @AssistedInject constructor( } private fun handleToggleTag(action: HomeRoomListAction.ToggleTag) { - session.getRoom(action.roomId)?.let { room -> - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { + session.getRoom(action.roomId)?.let { room -> try { if (room.roomSummary()?.hasTag(action.tag) == false) { // Favorite and low priority tags are exclusive, so maybe delete the other tag first @@ -390,6 +418,18 @@ class HomeRoomListViewModel @AssistedInject constructor( } } + private fun handleDeleteLocalRooms() = withState { + viewModelScope.launch { + val localRoomIds = session.roomService() + .getRoomSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Contains(RoomLocalEcho.PREFIX) }) + .map { it.roomId } + + localRoomIds.forEach { + session.roomService().deleteLocalRoom(it) + } + } + } + private fun String.otherTag(): String? { return when (this) { RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt index bfcaea22e9..ceaaf9b9dc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt @@ -18,7 +18,9 @@ package im.vector.app.features.home.room.list.home import com.airbnb.mvrx.MavericksState import im.vector.app.core.platform.StateView +import im.vector.app.features.home.room.list.home.header.RoomsHeadersData data class HomeRoomListViewState( - val state: StateView.State = StateView.State.Loading + val emptyState: StateView.State.Empty? = null, + val headersData: RoomsHeadersData = RoomsHeadersData(), ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt deleted file mode 100644 index 29df594d06..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.list.home - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter -import kotlinx.coroutines.flow.SharedFlow -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.util.Optional - -sealed class HomeRoomSection { - data class RoomSummaryData( - val list: LiveData>, - val filtersData: SharedFlow>>, - ) : HomeRoomSection() - - data class RecentRoomsData( - val list: LiveData> - ) : HomeRoomSection() - - data class InvitesCountData( - val count: LiveData - ) : HomeRoomSection() -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/NewChatBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/NewChatBottomSheet.kt index 05b86f7393..3f12ae4a63 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/NewChatBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/NewChatBottomSheet.kt @@ -16,43 +16,53 @@ package im.vector.app.features.home.room.list.home +import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.FragmentNewChatBottomSheetBinding import im.vector.app.features.navigation.Navigator import javax.inject.Inject @AndroidEntryPoint -class NewChatBottomSheet @Inject constructor() : BottomSheetDialogFragment() { +class NewChatBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var navigator: Navigator - private lateinit var binding: FragmentNewChatBottomSheetBinding + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentNewChatBottomSheetBinding { + return FragmentNewChatBottomSheetBinding.inflate(inflater, container, false) + } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentNewChatBottomSheetBinding.inflate(inflater, container, false) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { initFABs() - return binding.root } private fun initFABs() { - binding.startChat.setOnClickListener { + views.startChat.debouncedClicks { + dismiss() navigator.openCreateDirectRoom(requireActivity()) } - binding.createRoom.setOnClickListener { + views.createRoom.debouncedClicks { + dismiss() navigator.openCreateRoom(requireActivity()) } - binding.exploreRooms.setOnClickListener { + views.exploreRooms.debouncedClicks { + dismiss() navigator.openRoomDirectory(requireContext()) } } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setPeekHeightAsScreenPercentage(0.5f) + } + } + companion object { const val TAG = "NewChatBottomSheet" } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt index ce33440238..9bd800a47c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list.home.filter +package im.vector.app.features.home.room.list.home.header import androidx.annotation.StringRes import im.vector.app.R diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt new file mode 100644 index 0000000000..3cc058985a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list.home.header + +import android.content.res.Resources +import android.util.TypedValue +import androidx.recyclerview.widget.LinearLayoutManager +import com.airbnb.epoxy.Carousel +import com.airbnb.epoxy.CarouselModelBuilder +import com.airbnb.epoxy.EpoxyController +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.carousel +import com.google.android.material.color.MaterialColors +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.FirstItemUpdatedObserver +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.RoomListListener +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class HomeRoomsHeadersController @Inject constructor( + val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer, + resources: Resources, + private val analyticsTracker: AnalyticsTracker, +) : EpoxyController() { + + private var data: RoomsHeadersData = RoomsHeadersData() + + var onFilterChangedListener: ((HomeRoomFilter) -> Unit)? = null + var recentsRoomListener: RoomListListener? = null + var invitesClickListener: (() -> Unit)? = null + + private var carousel: Carousel? = null + + private var carouselAdapterObserver: FirstItemUpdatedObserver? = null + + private val recentsHPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 4f, + resources.displayMetrics + ).toInt() + + private val recentsTopPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 12f, + resources.displayMetrics + ).toInt() + + override fun buildModels() { + val host = this + if (data.invitesCount != 0) { + addInviteCounter(host.invitesClickListener, data.invitesCount) + } + + data.recents?.let { + addRecents(host, it) + } + + host.data.filtersList?.let { + addRoomFilterHeaderItem( + filterChangedListener = host.onFilterChangedListener, + filtersList = it, + currentFilter = host.data.currentFilter, + analyticsTracker = analyticsTracker) + } + } + + private fun addInviteCounter(invitesClickListener: (() -> Unit)?, invitesCount: Int) { + inviteCounterItem { + id("invites_counter") + invitesCount(invitesCount) + listener { invitesClickListener?.invoke() } + } + } + + private fun addRecents(host: HomeRoomsHeadersController, recents: List) { + carousel { + id("recents_carousel") + padding( + Carousel.Padding( + host.recentsHPadding, + host.recentsTopPadding, + host.recentsHPadding, + 0, + 0, + ) + ) + onBind { _, view, _ -> + host.carousel = view + host.unsubscribeAdapterObserver() + host.subscribeAdapterObserver() + + val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background) + view.setBackgroundColor(colorSurface) + } + + onUnbind { _, _ -> + host.carousel = null + host.unsubscribeAdapterObserver() + } + + withModelsFrom(recents) { roomSummary -> + val onClick = host.recentsRoomListener?.let { it::onRoomClicked } + val onLongClick = host.recentsRoomListener?.let { it::onRoomLongClicked } + + RecentRoomItem_() + .id(roomSummary.roomId) + .avatarRenderer(host.avatarRenderer) + .matrixItem(roomSummary.toMatrixItem()) + .unreadNotificationCount(roomSummary.notificationCount) + .showHighlighted(roomSummary.highlightCount > 0) + .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } + .itemClickListener { onClick?.invoke(roomSummary) } + } + } + } + + private fun unsubscribeAdapterObserver() { + carouselAdapterObserver?.let { observer -> + try { + carousel?.adapter?.unregisterAdapterDataObserver(observer) + carouselAdapterObserver = null + } catch (e: IllegalStateException) { + // do nothing + } + } + } + + private fun subscribeAdapterObserver() { + (carousel?.layoutManager as? LinearLayoutManager)?.let { layoutManager -> + carouselAdapterObserver = FirstItemUpdatedObserver(layoutManager) { + carousel?.post { + layoutManager.scrollToPosition(0) + } + }.also { observer -> + try { + carousel?.adapter?.registerAdapterDataObserver(observer) + } catch (e: IllegalStateException) { + // do nothing + } + } + } + } + + private fun addRoomFilterHeaderItem( + filterChangedListener: ((HomeRoomFilter) -> Unit)?, + filtersList: List, + currentFilter: HomeRoomFilter?, + analyticsTracker: AnalyticsTracker, + ) { + roomFilterHeaderItem { + id("filter_header") + filtersData(filtersList) + selectedFilter(currentFilter) + onFilterChangedListener(filterChangedListener) + analyticsTracker(analyticsTracker) + } + } + + fun submitData(data: RoomsHeadersData) { + this.data = data + requestModelBuild() + } +} + +private inline fun CarouselModelBuilder.withModelsFrom( + items: List, + modelBuilder: (T) -> EpoxyModel<*> +) { + models(items.map { modelBuilder(it) }) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt index 7536bde10a..5c43c86933 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list.home.invites +package im.vector.app.features.home.room.list.home.header import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt index d7c72ba5ed..9cd28624a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list.home.recent +package im.vector.app.features.home.room.list.home.header import android.view.HapticFeedbackConstants import android.view.View diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt similarity index 61% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt index bbe503806b..fd4333b722 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list.home.filter +package im.vector.app.features.home.room.list.home.header import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -22,6 +22,8 @@ import com.google.android.material.tabs.TabLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Interaction @EpoxyModelClass abstract class RoomFilterHeaderItem : VectorEpoxyModel(R.layout.item_home_filter_tabs) { @@ -32,18 +34,29 @@ abstract class RoomFilterHeaderItem : VectorEpoxyModel? = null + @EpoxyAttribute + var selectedFilter: HomeRoomFilter? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var analyticsTracker: AnalyticsTracker? = null + override fun bind(holder: Holder) { super.bind(holder) with(holder.tabLayout) { removeAllTabs() + clearOnTabSelectedListeners() filtersData?.forEach { filter -> - addTab(newTab().setText(filter.titleRes).setTag(filter)) + addTab( + newTab().setText(filter.titleRes).setTag(filter), + filter == (selectedFilter ?: HomeRoomFilter.ALL) + ) } addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { (tab?.tag as? HomeRoomFilter)?.let { filter -> + trackFilterChangeEvent(filter) onFilterChangedListener?.invoke(filter) } } @@ -54,6 +67,23 @@ abstract class RoomFilterHeaderItem : VectorEpoxyModel Interaction.Name.MobileAllChatsFilterAll + HomeRoomFilter.UNREADS -> Interaction.Name.MobileAllChatsFilterUnreads + HomeRoomFilter.FAVOURITES -> Interaction.Name.MobileAllChatsFilterFavourites + HomeRoomFilter.PEOPlE -> Interaction.Name.MobileAllChatsFilterPeople + } + + analyticsTracker?.capture( + Interaction( + index = null, + interactionType = null, + name = interactionName + ) + ) + } + override fun unbind(holder: Holder) { holder.tabLayout.clearOnTabSelectedListeners() super.unbind(holder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt new file mode 100644 index 0000000000..db7f8b9a96 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list.home.header + +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class RoomsHeadersData( + val invitesCount: Int = 0, + val filtersList: List? = null, + val currentFilter: HomeRoomFilter = HomeRoomFilter.ALL, + val recents: List? = null +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt deleted file mode 100644 index 82a31d30a9..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.list.home.invites - -import com.airbnb.epoxy.EpoxyController -import im.vector.app.core.resources.StringProvider -import javax.inject.Inject - -class InvitesCounterController @Inject constructor( - val stringProvider: StringProvider -) : EpoxyController() { - - private var count = 0 - var clickListener: (() -> Unit)? = null - - override fun buildModels() { - val host = this - if (count != 0) { - inviteCounterItem { - id("invites_counter") - invitesCount(host.count) - listener { host.clickListener?.invoke() } - } - } - } - - fun submitData(count: Int?) { - this.count = count ?: 0 - requestModelBuild() - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt index 0dbc1b8f34..ac39d7d567 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesFragment.kt @@ -27,6 +27,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentInvitesBinding +import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.notifications.NotificationDrawerManager @@ -48,6 +49,11 @@ class InvitesFragment : VectorBaseFragment(), RoomListLi return FragmentInvitesBinding.inflate(inflater, container, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = MobileScreen.ScreenName.Invites + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/layout/HomeLayoutSettingBottomDialogFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/layout/HomeLayoutSettingBottomDialogFragment.kt index 0c4d64a1cc..63b7f557e3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/layout/HomeLayoutSettingBottomDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/layout/HomeLayoutSettingBottomDialogFragment.kt @@ -25,6 +25,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetHomeLayoutSettingsBinding +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.list.home.HomeLayoutPreferencesStore import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -54,9 +55,11 @@ class HomeLayoutSettingBottomDialogFragment : VectorBaseBottomSheetDialogFragmen } views.homeLayoutSettingsRecents.setOnCheckedChangeListener { _, isChecked -> + trackRecentsStateEvent(isChecked) setRecentsEnabled(isChecked) } views.homeLayoutSettingsFilters.setOnCheckedChangeListener { _, isChecked -> + trackFiltersStateEvent(isChecked) setFiltersEnabled(isChecked) } views.homeLayoutSettingsSortGroup.setOnCheckedChangeListener { _, checkedId -> @@ -64,10 +67,40 @@ class HomeLayoutSettingBottomDialogFragment : VectorBaseBottomSheetDialogFragmen } } + private fun trackRecentsStateEvent(areEnabled: Boolean) { + val interactionName = if (areEnabled) { + Interaction.Name.MobileAllChatsRecentsEnabled + } else { + Interaction.Name.MobileAllChatsRecentsDisabled + } + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = interactionName + ) + ) + } + private fun setRecentsEnabled(isEnabled: Boolean) = lifecycleScope.launch { preferencesStore.setRecentsEnabled(isEnabled) } + private fun trackFiltersStateEvent(areEnabled: Boolean) { + val interactionName = if (areEnabled) { + Interaction.Name.MobileAllChatsFiltersEnabled + } else { + Interaction.Name.MobileAllChatsFiltersDisabled + } + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = interactionName + ) + ) + } + private fun setFiltersEnabled(isEnabled: Boolean) = lifecycleScope.launch { preferencesStore.setFiltersEnabled(isEnabled) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt deleted file mode 100644 index df5ce28da5..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.list.home.recent - -import android.content.res.Resources -import android.util.TypedValue -import com.airbnb.epoxy.Carousel -import com.airbnb.epoxy.CarouselModelBuilder -import com.airbnb.epoxy.EpoxyController -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.carousel -import com.google.android.material.color.MaterialColors -import im.vector.app.R -import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.list.RoomListListener -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.util.toMatrixItem -import javax.inject.Inject - -class RecentRoomCarouselController @Inject constructor( - private val avatarRenderer: AvatarRenderer, - private val resources: Resources, -) : EpoxyController() { - - private var data: List? = null - var listener: RoomListListener? = null - - private val hPadding = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 4f, - resources.displayMetrics - ).toInt() - - private val topPadding = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 12f, - resources.displayMetrics - ).toInt() - - fun submitList(recentList: List) { - this.data = recentList - requestModelBuild() - } - - override fun buildModels() { - val host = this - data?.let { data -> - carousel { - id("recents_carousel") - padding(Carousel.Padding( - host.hPadding, - host.topPadding, - host.hPadding, - 0, - 0, - ) - ) - onBind { _, view, _ -> - val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background) - view.setBackgroundColor(colorSurface) - } - withModelsFrom(data) { roomSummary -> - val onClick = host.listener?.let { it::onRoomClicked } - val onLongClick = host.listener?.let { it::onRoomLongClicked } - - RecentRoomItem_() - .id(roomSummary.roomId) - .avatarRenderer(host.avatarRenderer) - .matrixItem(roomSummary.toMatrixItem()) - .unreadNotificationCount(roomSummary.notificationCount) - .showHighlighted(roomSummary.highlightCount > 0) - .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } - .itemClickListener { onClick?.invoke(roomSummary) } - } - } - } - } -} - -private inline fun CarouselModelBuilder.withModelsFrom( - items: List, - modelBuilder: (T) -> EpoxyModel<*> -) { - models(items.map { modelBuilder(it) }) -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt index cefe107905..3320bdf314 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesPreferencesStore.kt @@ -34,7 +34,7 @@ class ReleaseNotesPreferencesStore @Inject constructor( private val context: Context ) { - private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_SHOWN") + private val isAppLayoutOnboardingShown = booleanPreferencesKey("SETTINGS_APP_LAYOUT_ONBOARDING_DISPLAYED") val appLayoutOnboardingShown: Flow = context.dataStore.data .map { preferences -> preferences[isAppLayoutOnboardingShown].orFalse() } 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 4eca377e28..636c557da9 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 @@ -16,29 +16,36 @@ package im.vector.app.features.homeserver -import android.content.Context +import android.content.SharedPreferences import androidx.core.content.edit import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject /** * Object to store and retrieve home and identity server urls. */ -object ServerUrlsRepository { +class ServerUrlsRepository @Inject constructor( + @DefaultPreferences + private val sharedPreferences: SharedPreferences, + private val stringProvider: StringProvider, +) { + companion object { + // Keys used to store default servers urls from the referrer + private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url" + private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url" - // Keys used to store default servers urls from the referrer - private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url" - private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url" - - // Keys used to store current homeserver url and identity url - const val HOME_SERVER_URL_PREF = "home_server_url" - const val IDENTITY_SERVER_URL_PREF = "identity_server_url" + // Keys used to store current homeserver url and identity url + const val HOME_SERVER_URL_PREF = "home_server_url" + const val IDENTITY_SERVER_URL_PREF = "identity_server_url" + } /** * Save home and identity sever urls received by the Referrer receiver. */ - fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) { - DefaultSharedPreferences.getInstance(context) + fun setDefaultUrlsFromReferrer(homeServerUrl: String, identityServerUrl: String) { + sharedPreferences .edit { if (homeServerUrl.isNotEmpty()) { putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl) @@ -53,8 +60,8 @@ object ServerUrlsRepository { /** * Save home and identity sever urls entered by the user. May be custom or default value. */ - fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) { - DefaultSharedPreferences.getInstance(context) + fun saveServerUrls(homeServerUrl: String, identityServerUrl: String) { + sharedPreferences .edit { putString(HOME_SERVER_URL_PREF, homeServerUrl) putString(IDENTITY_SERVER_URL_PREF, identityServerUrl) @@ -64,14 +71,12 @@ object ServerUrlsRepository { /** * Return last used homeserver url, or the default one from referrer or the default one from resources. */ - fun getLastHomeServerUrl(context: Context): String { - val prefs = DefaultSharedPreferences.getInstance(context) - - return prefs.getString( + fun getLastHomeServerUrl(): String { + return sharedPreferences.getString( HOME_SERVER_URL_PREF, - prefs.getString( + sharedPreferences.getString( DEFAULT_REFERRER_HOME_SERVER_URL_PREF, - getDefaultHomeServerUrl(context) + getDefaultHomeServerUrl() )!! )!! } @@ -79,10 +84,10 @@ object ServerUrlsRepository { /** * Return true if url is the default homeserver url form resources. */ - fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context) + fun isDefaultHomeServerUrl(url: String) = url == getDefaultHomeServerUrl() /** * Return default homeserver url from resources. */ - fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url) + fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) } diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 725f23cddd..9e869ecde1 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -27,8 +27,13 @@ package im.vector.app.features.html import android.content.Context import android.content.res.Resources +import android.graphics.drawable.Drawable import android.text.Spannable import androidx.core.text.toSpannable +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.request.target.Target +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences @@ -39,12 +44,15 @@ import io.noties.markwon.PrecomputedFutureTextSetterCompat import io.noties.markwon.ext.latex.JLatexMathPlugin import io.noties.markwon.ext.latex.JLatexMathTheme import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.image.AsyncDrawable +import io.noties.markwon.image.glide.GlideImagesPlugin import io.noties.markwon.inlineparser.EntityInlineProcessor import io.noties.markwon.inlineparser.HtmlInlineProcessor import io.noties.markwon.inlineparser.MarkwonInlineParser import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin import org.commonmark.node.Node import org.commonmark.parser.Parser +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -53,7 +61,8 @@ import javax.inject.Singleton class EventHtmlRenderer @Inject constructor( htmlConfigure: MatrixHtmlPluginConfigure, context: Context, - vectorPreferences: VectorPreferences + vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder ) { interface PostProcessor { @@ -62,6 +71,23 @@ class EventHtmlRenderer @Inject constructor( private val builder = Markwon.builder(context) .usePlugin(HtmlPlugin.create(htmlConfigure)) + .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { + override fun load(drawable: AsyncDrawable): RequestBuilder { + val url = drawable.destination + if (url.isMxcUrl()) { + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val imageUrl = contentUrlResolver.resolveFullSize(url) + // Override size to avoid crashes for huge pictures + return Glide.with(context).load(imageUrl).override(500) + } + // We don't want to support other url schemes here, so just return a request for null + return Glide.with(context).load(null as String?) + } + + override fun cancel(target: Target<*>) { + Glide.with(context).clear(target) + } + })) private val markwon = if (vectorPreferences.latexMathsIsEnabled()) { // If latex maths is enabled in app preferences, refomat it so Markwon recognises it diff --git a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt index 75ff2914c1..5bdd92dcf4 100644 --- a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -16,7 +16,6 @@ package im.vector.app.features.lifecycle -import android.annotation.SuppressLint import android.app.Activity import android.app.ActivityManager import android.app.Application @@ -69,7 +68,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager // Get all activities from PermissionController module // See https://source.android.com/docs/core/architecture/modular-system/permissioncontroller#package-format - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) { activitiesInfo += tryOrNull { packageManager.getPackageInfo("com.google.android.permissioncontroller", PackageManager.GET_ACTIVITIES).activities } ?: tryOrNull { @@ -112,8 +111,6 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager * * @return true if an app task is corrupted by a potentially malicious activity */ - @SuppressLint("NewApi") - @Suppress("DEPRECATION") private suspend fun isTaskCorrupted(activity: Activity): Boolean = withContext(Dispatchers.Default) { val context = activity.applicationContext @@ -135,6 +132,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager // This was present in ActivityManager.RunningTaskInfo class since API level 1! // and it is inherited from TaskInfo since Android Q (API level 29). // API 29 changes : https://developer.android.com/sdk/api_diff/29/changes/android.app.ActivityManager.RunningTaskInfo + @Suppress("DEPRECATION") manager.getRunningTasks(10).any { runningTaskInfo -> runningTaskInfo.topActivity?.let { // Check whether the activity task affinity matches with app task affinity. 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 25403b06f3..b082e37933 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 @@ -144,7 +144,6 @@ class LoginCaptchaFragment : // runOnUiThread(Runnable { finish() }) } - @SuppressLint("NewApi") override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { super.onReceivedHttpError(view, request, errorResponse) diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 9994ae7e0a..67778eed65 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -40,7 +40,6 @@ import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequests import im.vector.app.core.ui.model.Size import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.settings.VectorPreferences import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.toScanFailure @@ -68,7 +67,6 @@ class ImageContentRenderer @Inject constructor( private val localFilesHelper: LocalFilesHelper, private val activeSessionHolder: ActiveSessionHolder, private val dimensionConverter: DimensionConverter, - private val vectorPreferences: VectorPreferences ) { @Parcelize @@ -88,6 +86,7 @@ class ImageContentRenderer @Inject constructor( enum class Mode { FULL_SIZE, + ANIMATED_THUMBNAIL, THUMBNAIL, STICKER } @@ -163,7 +162,7 @@ class ImageContentRenderer @Inject constructor( createGlideRequest(data, mode, imageView, size) .let { - if (vectorPreferences.autoplayAnimatedImages()) it + if (mode == Mode.ANIMATED_THUMBNAIL) it else it.dontAnimate() } .transform(cornerTransformation) @@ -262,6 +261,7 @@ class ImageContentRenderer @Inject constructor( val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = when (mode) { Mode.FULL_SIZE, + Mode.ANIMATED_THUMBNAIL, Mode.STICKER -> resolveUrl(data) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } @@ -300,6 +300,7 @@ class ImageContentRenderer @Inject constructor( finalHeight = height finalWidth = width } + Mode.ANIMATED_THUMBNAIL, Mode.THUMBNAIL -> { finalHeight = min(maxImageWidth * height / width, maxImageHeight) finalWidth = finalHeight * width / height diff --git a/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt b/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt index 5c751e0d55..6c9b8b2e6c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt @@ -29,13 +29,13 @@ class CircularCache(cacheSize: Int, factory: (Int) -> Array) { private val cache = factory(cacheSize) private var writeIndex = 0 - fun contains(key: T): Boolean = cache.contains(key) + fun contains(value: T): Boolean = cache.contains(value) - fun put(key: T) { + fun put(value: T) { if (writeIndex == cache.size) { writeIndex = 0 } - cache[writeIndex] = key + cache[writeIndex] = value writeIndex++ } } 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 ae5a8aec7d..90138fd495 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 @@ -213,7 +213,8 @@ class NotifiableEventResolver @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (ignore: MXCryptoError) { } 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 547f89a7c4..36a66e01ec 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 @@ -19,7 +19,6 @@ package im.vector.app.features.notifications import android.annotation.SuppressLint -import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -34,6 +33,7 @@ import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import androidx.annotation.AttrRes +import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.app.NotificationCompat @@ -102,6 +102,7 @@ class NotificationUtils @Inject constructor( const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) fun openSystemSettingsForSilentCategory(fragment: Fragment) { @@ -126,7 +127,6 @@ class NotificationUtils @Inject constructor( /** * Create notification channels. */ - @TargetApi(Build.VERSION_CODES.O) fun createNotificationChannels() { if (!supportNotificationChannels()) { return @@ -218,7 +218,6 @@ class NotificationUtils @Inject constructor( * @param withProgress true to show indeterminate progress on the notification * @return the polling thread listener notification */ - @SuppressLint("NewApi") fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { // build the pending intent go to the home screen if this is clicked. val i = HomeActivity.newIntent(context, firstStartMainActivity = false) @@ -287,7 +286,6 @@ class NotificationUtils @Inject constructor( * @param fromBg true if the app is in background when posting the notification * @return the call notification. */ - @SuppressLint("NewApi") fun buildIncomingCallNotification( call: WebRtcCall, title: String, @@ -420,7 +418,6 @@ class NotificationUtils @Inject constructor( * @param title title of the notification * @return the call notification. */ - @SuppressLint("NewApi") fun buildPendingCallNotification( call: WebRtcCall, title: String @@ -966,6 +963,7 @@ class NotificationUtils @Inject constructor( } } + @SuppressLint("LaunchActivityFromNotification") fun displayDiagnosticNotification() { val testActionIntent = Intent(context, TestNotificationReceiver::class.java) testActionIntent.action = actionIds.diagnostic 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 23c6c13b5e..11f257c4e8 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 @@ -92,7 +92,6 @@ class CaptchaWebview @Inject constructor( Timber.e("## onError() : $errorMessage") } - @SuppressLint("NewApi") override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { super.onReceivedHttpError(view, request, errorResponse) when { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 161c065161..161bad81a5 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -58,7 +58,7 @@ import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms import org.matrix.android.sdk.api.extensions.tryOrNull -// Tchap: custom tag to separate the reset password screens from the initial login screen +// Tchap : custom tag to separate the reset password screens from the initial login screen private const val TCHAP_FRAGMENT_LOGIN_STAGE_TAG = "TCHAP_FRAGMENT_LOGIN_STAGE_TAG" private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt index 9bcf6e4264..026ee159ed 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.features.pin.lockscreen.biometrics -import android.annotation.SuppressLint import android.content.Context import android.os.Build import androidx.annotation.MainThread @@ -156,7 +155,6 @@ class BiometricHelper @AssistedInject constructor( return authenticate(activity) } - @SuppressLint("NewApi") @OptIn(ExperimentalCoroutinesApi::class) private fun authenticateInternal( activity: FragmentActivity, diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt index a42ce3a9b7..fd676f1662 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCrypto.kt @@ -16,7 +16,6 @@ package im.vector.app.features.pin.lockscreen.crypto -import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.security.keystore.KeyPermanentlyInvalidatedException @@ -55,7 +54,6 @@ class KeyStoreCrypto @AssistedInject constructor( * Ensures a [Key] for the [alias] exists and validates it. * @throws KeyPermanentlyInvalidatedException if key is not valid. */ - @SuppressLint("NewApi") @Throws(KeyPermanentlyInvalidatedException::class) fun ensureKey() = secretStoringUtils.ensureKey(alias).also { // Check validity of Key by initializing an encryption Cipher @@ -109,10 +107,9 @@ class KeyStoreCrypto @AssistedInject constructor( /** * Check if the key associated with the [alias] is valid. */ - @SuppressLint("NewApi") fun hasValidKey(): Boolean { val keyExists = hasKey() - return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) { + return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) && keyExists) { val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() } initializedKey != null } else { diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt index bb55ceb1b7..c2d70a3734 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeysMigrator.kt @@ -16,7 +16,6 @@ package im.vector.app.features.pin.lockscreen.crypto -import android.annotation.SuppressLint import android.os.Build import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator @@ -36,14 +35,13 @@ class LockScreenKeysMigrator @Inject constructor( /** * Performs any needed migrations in order. */ - @SuppressLint("NewApi") suspend fun migrateIfNeeded() { if (legacyPinCodeMigrator.isMigrationNeeded()) { legacyPinCodeMigrator.migrate() missingSystemKeyMigrator.migrateIfNeeded() } - if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.get() >= Build.VERSION_CODES.M) { + if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.isAtLeast(Build.VERSION_CODES.M)) { systemKeyV1Migrator.migrate() } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt index 4c33c14954..7593aa6de3 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/migrations/MissingSystemKeyMigrator.kt @@ -16,7 +16,6 @@ package im.vector.app.features.pin.lockscreen.crypto.migrations -import android.annotation.SuppressLint import android.os.Build import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias @@ -38,9 +37,9 @@ class MissingSystemKeyMigrator @Inject constructor( /** * If user had biometric auth enabled, ensure system key exists, creating one if needed. */ - @SuppressLint("NewApi") fun migrateIfNeeded() { - if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) { + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) && + vectorPreferences.useBiometricsToUnlock()) { val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, true) runCatching { systemKeyStoreCrypto.ensureKey() diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt index 33ea590f1d..87d3f93f9b 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.pin.lockscreen.ui -import android.annotation.SuppressLint import android.app.KeyguardManager import android.os.Build import android.security.keystore.KeyPermanentlyInvalidatedException @@ -139,12 +138,12 @@ class LockScreenViewModel @AssistedInject constructor( } }.launchIn(viewModelScope) - @SuppressLint("NewApi") private fun showBiometricPrompt(activity: FragmentActivity) = flow { emitAll(biometricHelper.authenticate(activity)) }.catch { error -> when { - versionProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException -> { + versionProvider.isAtLeast(Build.VERSION_CODES.M) && + error is KeyPermanentlyInvalidatedException -> { onBiometricKeyInvalidated() } else -> { @@ -168,15 +167,14 @@ class LockScreenViewModel @AssistedInject constructor( _viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage) } - @SuppressLint("NewApi") private suspend fun updateStateWithBiometricInfo() { // This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+ waitUntilKeyguardIsUnlocked() setState { val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid val canUseBiometricAuth = lockScreenConfiguration.mode == LockScreenMode.VERIFY && - !isSystemAuthTemporarilyDisabledByBiometricPrompt && - biometricHelper.isSystemAuthEnabledAndValid + !isSystemAuthTemporarilyDisabledByBiometricPrompt && + biometricHelper.isSystemAuthEnabledAndValid val showBiometricPromptAutomatically = canUseBiometricAuth && lockScreenConfiguration.autoStartBiometric copy( canUseBiometricAuth = canUseBiometricAuth, @@ -191,12 +189,12 @@ class LockScreenViewModel @AssistedInject constructor( * after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes. * See issue [#6768](https://github.com/vector-im/element-android/issues/6768). */ - @SuppressLint("NewApi") private suspend fun waitUntilKeyguardIsUnlocked() { - if (versionProvider.get() < Build.VERSION_CODES.S) return - withTimeoutOrNull(5.seconds) { - while (keyguardManager.isDeviceLocked) { - delay(50.milliseconds) + if (versionProvider.isAtLeast(Build.VERSION_CODES.S)) { + withTimeoutOrNull(5.seconds) { + while (keyguardManager.isDeviceLocked) { + delay(50.milliseconds) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragment.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragment.kt index d50ff791ed..c778e880b2 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragment.kt @@ -50,10 +50,10 @@ class FallbackBiometricDialogFragment : DialogFragment(R.layout.fragment_biometr private val parsedArgs by args() - @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + @Suppress("DEPRECATION") retainInstance = true setStyle(STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog) diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt index 9271f9838c..14d34b659e 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt @@ -39,13 +39,13 @@ class VerificationVectorAlert( override var colorAttribute: Int? = R.attr.colorPrimary class ViewBinder( - private val matrixItem: MatrixItem?, + private val matrixItem: MatrixItem, private val avatarRenderer: AvatarRenderer ) : VectorAlert.ViewBinder { override fun bind(view: View) { val views = AlerterVerificationLayoutBinding.bind(view) - matrixItem?.let { avatarRenderer.render(it, views.ivUserAvatar, GlideApp.with(view.context.applicationContext)) } + avatarRenderer.render(matrixItem, views.ivUserAvatar, GlideApp.with(view.context.applicationContext)) } } } 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 a4f173d2f8..7073afa768 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 @@ -31,7 +31,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.getAllChildFragments import im.vector.app.core.extensions.toOnOff import im.vector.app.core.resources.BuildMeta -import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.settings.VectorLocaleProvider import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devtools.GossipingEventsSerializer import im.vector.app.features.settings.locale.SystemLocaleProvider @@ -80,6 +80,7 @@ class BugReporter @Inject constructor( private val buildMeta: BuildMeta, private val processInfo: ProcessInfo, private val sdkIntProvider: BuildVersionSdkIntProvider, + private val vectorLocale: VectorLocaleProvider, ) { var inMultiWindowMode = false @@ -303,7 +304,7 @@ class BugReporter @Inject constructor( Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME ) .addFormDataPart("locale", Locale.getDefault().toString()) - .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) + .addFormDataPart("app_language", vectorLocale.applicationLocale.toString()) .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) .addFormDataPart("server_version", serverVersion) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt index 5496ff4a94..23b4fe04a8 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt @@ -16,10 +16,10 @@ package im.vector.app.features.rageshake -import android.content.Context +import android.content.SharedPreferences import android.os.Build import androidx.core.content.edit -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.resources.VersionCodeProvider import im.vector.app.features.version.VersionProvider import org.matrix.android.sdk.api.Matrix @@ -31,10 +31,11 @@ import javax.inject.Singleton @Singleton class VectorUncaughtExceptionHandler @Inject constructor( - context: Context, + @DefaultPreferences + private val preferences: SharedPreferences, private val bugReporter: BugReporter, private val versionProvider: VersionProvider, - private val versionCodeProvider: VersionCodeProvider + private val versionCodeProvider: VersionCodeProvider, ) : Thread.UncaughtExceptionHandler { // key to save the crash status @@ -44,8 +45,6 @@ class VectorUncaughtExceptionHandler @Inject constructor( private var previousHandler: Thread.UncaughtExceptionHandler? = null - private val preferences = DefaultSharedPreferences.getInstance(context) - /** * Activate this handler. */ 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 2894cd4621..65d28a5ceb 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 @@ -59,7 +59,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorPr import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -235,23 +235,27 @@ class RoomMemberProfileFragment : if (state.userMXCrossSigningInfo.isTrusted()) { // User is trusted if (state.allDevicesAreCrossSignedTrusted) { - RoomEncryptionTrustLevel.Trusted + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED } else { - RoomEncryptionTrustLevel.Warning + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED } } else { - RoomEncryptionTrustLevel.Default + if (state.userMXCrossSigningInfo.wasTrustedOnce) { + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + } else { + UserVerificationLevel.WAS_NEVER_VERIFIED + } } } else { // Legacy if (state.allDevicesAreTrusted) { - RoomEncryptionTrustLevel.Trusted + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED } else { - RoomEncryptionTrustLevel.Warning + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED } } - headerViews.memberProfileDecorationImageView.render(trustLevel) - views.matrixProfileDecorationToolbarAvatarImageView.render(trustLevel) + headerViews.memberProfileDecorationImageView.renderUser(trustLevel) + views.matrixProfileDecorationToolbarAvatarImageView.renderUser(trustLevel) } else { headerViews.memberProfileDecorationImageView.isVisible = false } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index 22b040b4c0..44bac1c8a0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -27,4 +27,5 @@ sealed class RoomProfileAction : VectorViewModelAction { object ShareRoomProfile : RoomProfileAction() object CreateShortcut : RoomProfileAction() object RestoreEncryptionState : RoomProfileAction() + data class SetEncryptToVerifiedDeviceOnly(val enabled: Boolean) : RoomProfileAction() } 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 f2d2365e49..814224ecc7 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 @@ -29,6 +29,7 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericPositiveButtonItem +import im.vector.app.features.form.formSwitchItem import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod @@ -68,6 +69,8 @@ class RoomProfileController @Inject constructor( fun onUrlInTopicLongClicked(url: String) fun doMigrateToVersion(newVersion: String) fun restoreEncryptionState() + fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean) + fun openGlobalBlockSettings() } override fun buildModels(data: RoomProfileViewState?) { @@ -180,6 +183,53 @@ class RoomProfileController @Inject constructor( // Hidden in Tchap // buildEncryptionAction(data.actionPermissions, roomSummary) + if (roomSummary.isEncrypted && !encryptionMisconfigured) { + data.globalCryptoConfig.invoke()?.let { globalConfig -> + if (globalConfig.globalBlockUnverifiedDevices) { + genericFooterItem { + id("globalConfig") + centered(false) + text( + span { + +host.stringProvider.getString(R.string.room_settings_global_block_unverified_info_text) + apply { + if (data.unverifiedDevicesInTheRoom.invoke() == true) { + +"\n" + +host.stringProvider.getString(R.string.some_devices_will_not_be_able_to_decrypt) + } + } + }.toEpoxyCharSequence() + ) + itemClickAction { + host.callback?.openGlobalBlockSettings() + } + } + } else { + // per room setting is available + val shouldBlockUnverified = data.encryptToVerifiedDeviceOnly.invoke() + formSwitchItem { + id("send_to_unverified") + enabled(shouldBlockUnverified != null) + title(host.stringProvider.getString(R.string.encryption_never_send_to_unverified_devices_in_room)) + + switchChecked(shouldBlockUnverified ?: false) + + apply { + if (shouldBlockUnverified == true && data.unverifiedDevicesInTheRoom.invoke() == true) { + summary( + host.stringProvider.getString(R.string.some_devices_will_not_be_able_to_decrypt) + ) + } else { + summary(null) + } + } + listener { value -> + host.callback?.setEncryptedToVerifiedDevicesOnly(value) + } + } + } + } + } // More buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) if (roomType != TchapRoomType.DIRECT) { 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 f4d0adc8bf..fe8e10ff22 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 @@ -52,6 +52,7 @@ import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.app.features.navigation.SettingsActivityPayload import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -354,6 +355,14 @@ class RoomProfileFragment : ) } + override fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean) { + roomProfileViewModel.handle(RoomProfileAction.SetEncryptToVerifiedDeviceOnly(enabled)) + } + + override fun openGlobalBlockSettings() { + navigator.openSettings(requireContext(), SettingsActivityPayload.SecurityPrivacy) + } + private fun onAvatarClicked(view: View) = withState(roomProfileViewModel) { state -> state.roomSummary()?.toMatrixItem()?.let { matrixItem -> navigator.openBigImageViewer(requireActivity(), view, matrixItem) 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 8ef30310ae..2f1b521d53 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 @@ -17,6 +17,7 @@ package im.vector.app.features.roomprofile +import androidx.lifecycle.asFlow import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -34,7 +35,10 @@ import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue @@ -82,6 +86,45 @@ class RoomProfileViewModel @AssistedInject constructor( observePermissions() observePowerLevels() observeAdminMembers() + observeCryptoSettings(flowRoom) + } + + private fun observeCryptoSettings(flowRoom: FlowRoom) { + val perRoomBlockStatus = session.cryptoService().getLiveBlockUnverifiedDevices(initialState.roomId) + .asFlow() + + perRoomBlockStatus + .execute { + copy(encryptToVerifiedDeviceOnly = it) + } + + val globalBlockStatus = session.cryptoService().getLiveGlobalCryptoConfig() + .asFlow() + + globalBlockStatus + .execute { + copy(globalCryptoConfig = it) + } + + perRoomBlockStatus.combine(globalBlockStatus) { perRoom, global -> + perRoom || global.globalBlockUnverifiedDevices + }.flatMapLatest { + if (it) { + flowRoom.liveRoomMembers(roomMemberQueryParams { memberships = Membership.activeMemberships() }) + .map { it.map { it.userId } } + .flatMapLatest { + session.cryptoService().getLiveCryptoDeviceInfo(it).asFlow() + } + } else { + flowOf(emptyList()) + } + }.map { + it.isNotEmpty() + }.execute { + copy( + unverifiedDevicesInTheRoom = it + ) + } } private fun observePowerLevels() { @@ -175,6 +218,7 @@ class RoomProfileViewModel @AssistedInject constructor( is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() RoomProfileAction.CreateShortcut -> handleCreateShortcut() RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState() + is RoomProfileAction.SetEncryptToVerifiedDeviceOnly -> setEncryptToVerifiedDeviceOnly(action.enabled) } } @@ -246,6 +290,12 @@ class RoomProfileViewModel @AssistedInject constructor( } } + private fun setEncryptToVerifiedDeviceOnly(enabled: Boolean) { + session.coroutineScope.launch { + session.cryptoService().setRoomBlockUnverifiedDevices(room.roomId, enabled) + } + } + private fun restoreEncryptionState() { _viewEvents.post(RoomProfileViewEvents.Loading()) session.coroutineScope.launch { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 2eb3141014..2fadb3277a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -20,6 +20,7 @@ package im.vector.app.features.roomprofile import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent @@ -36,7 +37,10 @@ data class RoomProfileViewState( val recommendedRoomVersion: String? = null, val canUpgradeRoom: Boolean = false, val isTombstoned: Boolean = false, - val canUpdateRoomState: Boolean = false + val canUpdateRoomState: Boolean = false, + val encryptToVerifiedDeviceOnly: Async = Uninitialized, + val globalCryptoConfig: Async = Uninitialized, + val unverifiedDevicesInTheRoom: Async = Uninitialized, ) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 8f310a6a89..9adfeb2a0e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -129,7 +129,7 @@ class RoomMemberListController @Inject constructor( id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) clickListener { host.callback?.onRoomMemberClicked(roomMember) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 915ce51d91..9ddcde7e4a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -37,7 +37,8 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom @@ -116,14 +117,7 @@ class RoomMemberListViewModel @AssistedInject constructor( .map { deviceList -> // If any key change, emit the userIds list deviceList.groupBy { it.userId }.mapValues { - val allDeviceTrusted = it.value.fold(it.value.isNotEmpty()) { prev, next -> - prev && next.trustLevel?.isCrossSigningVerified().orFalse() - } - if (session.cryptoService().crossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) { - if (allDeviceTrusted) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Warning - } else { - RoomEncryptionTrustLevel.Default - } + getUserTrustLevel(it.key, it.value) } } } @@ -133,6 +127,29 @@ class RoomMemberListViewModel @AssistedInject constructor( } } + private fun getUserTrustLevel(userId: String, devices: List): UserVerificationLevel { + val allDeviceTrusted = devices.fold(devices.isNotEmpty()) { prev, next -> + prev && next.trustLevel?.isCrossSigningVerified().orFalse() + } + val mxCrossSigningInfo = session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId) + return when { + mxCrossSigningInfo == null -> { + UserVerificationLevel.WAS_NEVER_VERIFIED + } + mxCrossSigningInfo.isTrusted() -> { + if (allDeviceTrusted) UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED + else UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED + } + else -> { + if (mxCrossSigningInfo.wasTrustedOnce) { + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + } else { + UserVerificationLevel.WAS_NEVER_VERIFIED + } + } + } + } + private fun observePowerLevel() { PowerLevelsFlowFactory(room).createFlow() .onEach { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 3cea47e60d..7861970c28 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -23,7 +23,7 @@ import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.platform.GenericIdArgs import im.vector.app.features.roomprofile.RoomProfileArgs -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -36,7 +36,7 @@ data class RoomMemberListViewState( val ignoredUserIds: List = emptyList(), val filter: String = "", val threePidInvites: Async> = Uninitialized, - val trustLevelMap: Async> = Uninitialized, + val trustLevelMap: Async> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() ) : MavericksState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index f53f572e38..98a28557ae 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -82,15 +82,18 @@ class RoomUploadsMediaFragment : controller.listener = this } - @Suppress("DEPRECATION") private fun getNumberOfColumns(): Int { - val displayMetrics = DisplayMetrics() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - requireContext().display?.getMetrics(displayMetrics) + val screenWidthInPx = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val a = requireActivity().windowManager.currentWindowMetrics + a.bounds.width() } else { + val displayMetrics = DisplayMetrics() + @Suppress("DEPRECATION") requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + displayMetrics.widthPixels } - return dimensionConverter.pxToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP + val screenWidthInDp = dimensionConverter.pxToDp(screenWidthInPx) + return screenWidthInDp / IMAGE_SIZE_DP } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt b/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt index 292d0107ba..34862adc4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/FontScalePreferences.kt @@ -57,6 +57,16 @@ interface FontScalePreferences { * @return list of values */ fun getAvailableScales(): List + + companion object { + const val SCALE_TINY = 0.70f + const val SCALE_SMALL = 0.85f + const val SCALE_NORMAL = 1.00f + const val SCALE_LARGE = 1.15f + const val SCALE_LARGER = 1.30f + const val SCALE_LARGEST = 1.45f + const val SCALE_HUGE = 1.60f + } } /** @@ -73,13 +83,13 @@ class FontScalePreferencesImpl @Inject constructor( } private val fontScaleValues = listOf( - FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny), - FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small), - FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal), - FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large), - FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger), - FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest), - FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge) + FontScaleValue(0, "FONT_SCALE_TINY", FontScalePreferences.SCALE_TINY, R.string.tiny), + FontScaleValue(1, "FONT_SCALE_SMALL", FontScalePreferences.SCALE_SMALL, R.string.small), + FontScaleValue(2, "FONT_SCALE_NORMAL", FontScalePreferences.SCALE_NORMAL, R.string.normal), + FontScaleValue(3, "FONT_SCALE_LARGE", FontScalePreferences.SCALE_LARGE, R.string.large), + FontScaleValue(4, "FONT_SCALE_LARGER", FontScalePreferences.SCALE_LARGER, R.string.larger), + FontScaleValue(5, "FONT_SCALE_LARGEST", FontScalePreferences.SCALE_LARGEST, R.string.largest), + FontScaleValue(6, "FONT_SCALE_HUGE", FontScalePreferences.SCALE_HUGE, R.string.huge) ) private val normalFontScaleValue = fontScaleValues[2] 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 177934c129..e4bef53660 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 @@ -17,30 +17,40 @@ package im.vector.app.features.settings import android.content.Context +import android.content.SharedPreferences import android.content.res.Configuration import androidx.core.content.edit import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.resources.BuildMeta import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import java.util.IllformedLocaleException import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton /** * Object to manage the Locale choice of the user. */ -object VectorLocale { - private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" - private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" - private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" - private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" +@Singleton +class VectorLocale @Inject constructor( + private val context: Context, + private val buildMeta: BuildMeta, + @DefaultPreferences + private val preferences: SharedPreferences, +) { + companion object { + const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" + const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" + const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" + private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" + private const val ISO_15924_LATN = "Latn" + } private val defaultLocale = Locale("fr", "FR") - private const val ISO_15924_LATN = "Latn" - /** * The cache of supported application languages. */ @@ -52,17 +62,10 @@ object VectorLocale { var applicationLocale = defaultLocale private set - private lateinit var context: Context - private lateinit var buildMeta: BuildMeta - /** - * Init this object. + * Init this singleton. */ - fun init(context: Context, buildMeta: BuildMeta) { - this.context = context - this.buildMeta = buildMeta - val preferences = DefaultSharedPreferences.getInstance(context) - + fun init() { if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { applicationLocale = Locale( preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, @@ -88,7 +91,7 @@ object VectorLocale { fun saveApplicationLocale(locale: Locale) { applicationLocale = locale - DefaultSharedPreferences.getInstance(context).edit { + preferences.edit { val language = locale.language if (language.isEmpty()) { remove(APPLICATION_LOCALE_LANGUAGE_KEY) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorLocaleProvider.kt b/vector/src/main/java/im/vector/app/features/settings/VectorLocaleProvider.kt new file mode 100644 index 0000000000..cbfa92d048 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/VectorLocaleProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings + +import android.content.SharedPreferences +import im.vector.app.core.di.DefaultPreferences +import java.util.Locale +import javax.inject.Inject + +/** + * Class to provide the Locale choice of the user. + */ +class VectorLocaleProvider @Inject constructor( + @DefaultPreferences + private val preferences: SharedPreferences, +) { + /** + * Get the current local. + * SharedPref values has been initialized in [VectorLocale.init] + */ + val applicationLocale: Locale + get() = Locale( + preferences.getString(VectorLocale.APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, + preferences.getString(VectorLocale.APPLICATION_LOCALE_COUNTRY_KEY, "")!!, + preferences.getString(VectorLocale.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 d1caa03d25..605ee05459 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 @@ -25,8 +25,9 @@ import androidx.core.content.edit import com.squareup.seismic.ShakeDetector import im.vector.app.R import im.vector.app.config.Config -import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.resources.BuildMeta +import im.vector.app.core.resources.StringProvider import im.vector.app.core.time.Clock import im.vector.app.features.VectorFeatures import im.vector.app.features.disclaimer.SHARED_PREF_KEY @@ -42,6 +43,9 @@ class VectorPreferences @Inject constructor( private val clock: Clock, private val buildMeta: BuildMeta, private val vectorFeatures: VectorFeatures, + @DefaultPreferences + private val defaultPrefs: SharedPreferences, + private val stringProvider: StringProvider, ) { companion object { @@ -67,6 +71,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" const val SETTINGS_LABS_NEW_APP_LAYOUT_KEY = "SETTINGS_LABS_NEW_APP_LAYOUT_KEY" + const val SETTINGS_LABS_DEFERRED_DM_KEY = "SETTINGS_LABS_DEFERRED_DM_KEY" const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" @@ -83,6 +88,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY" const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" const val SETTINGS_PERSISTED_SPACE_BACKSTACK = "SETTINGS_PERSISTED_SPACE_BACKSTACK" + const val SETTINGS_SECURITY_INCOGNITO_KEYBOARD_PREFERENCE_KEY = "SETTINGS_SECURITY_INCOGNITO_KEYBOARD_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT = "SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT" // const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY" @@ -289,13 +295,12 @@ class VectorPreferences @Inject constructor( SETTINGS_USE_RAGE_SHAKE_KEY, SETTINGS_SECURITY_USE_FLAG_SECURE, + SETTINGS_SECURITY_INCOGNITO_KEYBOARD_PREFERENCE_KEY, ShortcutsHandler.SHARED_PREF_KEY, ) } - private val defaultPrefs = DefaultSharedPreferences.getInstance(context) - /** * Allow subscribing and unsubscribing to configuration changes. This is * particularly useful when you need to be notified of a configuration change @@ -721,10 +726,10 @@ class VectorPreferences @Inject constructor( */ fun getSelectedMediasSavingPeriodString(): String { return when (getSelectedMediasSavingPeriod()) { - MEDIA_SAVING_3_DAYS -> context.getString(R.string.media_saving_period_3_days) - MEDIA_SAVING_1_WEEK -> context.getString(R.string.media_saving_period_1_week) - MEDIA_SAVING_1_MONTH -> context.getString(R.string.media_saving_period_1_month) - MEDIA_SAVING_FOREVER -> context.getString(R.string.media_saving_period_forever) + MEDIA_SAVING_3_DAYS -> stringProvider.getString(R.string.media_saving_period_3_days) + MEDIA_SAVING_1_WEEK -> stringProvider.getString(R.string.media_saving_period_1_week) + MEDIA_SAVING_1_MONTH -> stringProvider.getString(R.string.media_saving_period_1_month) + MEDIA_SAVING_FOREVER -> stringProvider.getString(R.string.media_saving_period_forever) else -> "?" } } @@ -973,6 +978,11 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_FLAG_SECURE, true) } + /** Whether the keyboard should disable personalized learning. */ + fun useIncognitoKeyboard(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SECURITY_INCOGNITO_KEYBOARD_PREFERENCE_KEY, false) + } + /** * The user enable protecting app access with pin code. * Currently we use the pin code store to know if the pin is enabled, so this is not used @@ -1170,6 +1180,13 @@ class VectorPreferences @Inject constructor( defaultPrefs.getBoolean(SETTINGS_LABS_NEW_APP_LAYOUT_KEY, getDefault(R.bool.settings_labs_new_app_layout_default)) } + /** + * Indicates whether or not deferred DMs are enabled. + */ + fun isDeferredDmEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_DEFERRED_DM_KEY, getDefault(R.bool.settings_labs_deferred_dm_default)) + } + fun showLiveSenderInfo(): Boolean { return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default)) } 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 3c8ec56713..073d5f7468 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 @@ -46,6 +46,7 @@ class VectorSettingsPreferencesFragment : @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var fontScalePreferences: FontScalePreferences @Inject lateinit var vectorFeatures: VectorFeatures + @Inject lateinit var vectorLocale: VectorLocale override var titleRes = R.string.settings_preferences override val preferenceXmlRes = R.xml.vector_settings_preferences @@ -198,7 +199,7 @@ class VectorSettingsPreferencesFragment : private fun setUserInterfacePreferences() { // Selected language - selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) + selectedLanguagePreference.summary = vectorLocale.localeToLocalisedString(vectorLocale.applicationLocale) // Text size textSizePreference.summary = getString(fontScalePreferences.getResolvedFontScaleValue().nameResId) 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 e953de1916..ec2994a745 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 @@ -17,10 +17,10 @@ package im.vector.app.features.settings -import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -162,6 +162,10 @@ class VectorSettingsSecurityPrivacyFragment : findPreference("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!! } + private val incognitoKeyboardPref by lazy { + findPreference(VectorPreferences.SETTINGS_SECURITY_INCOGNITO_KEYBOARD_PREFERENCE_KEY)!! + } + 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 @@ -278,6 +282,9 @@ class VectorSettingsSecurityPrivacyFragment : // Analytics setUpAnalytics() + // Incognito Keyboard + setUpIncognitoKeyboard() + // Pin code openPinCodeSettingsPref.setOnPreferenceClickListener { openPinCodePreferenceScreen() @@ -340,6 +347,10 @@ class VectorSettingsSecurityPrivacyFragment : } } + private fun setUpIncognitoKeyboard() { + incognitoKeyboardPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + } + // Todo this should be refactored and use same state as 4S section private fun refreshXSigningStatus() { if (BuildConfig.ENABLE_CROSS_SIGNING) { @@ -451,7 +462,6 @@ class VectorSettingsSecurityPrivacyFragment : /** * Manage the e2e keys import. */ - @SuppressLint("NewApi") private fun importKeys() { openFileSelection( requireActivity(), diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsVoiceVideoFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsVoiceVideoFragment.kt index fbf54479fc..28e167779d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsVoiceVideoFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsVoiceVideoFragment.kt @@ -23,17 +23,19 @@ import android.net.Uri import android.os.Bundle import androidx.preference.Preference import androidx.preference.SwitchPreference +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.preference.VectorPreference -import im.vector.app.core.utils.getCallRingtoneName -import im.vector.app.core.utils.getCallRingtoneUri -import im.vector.app.core.utils.setCallRingtoneUri -import im.vector.app.core.utils.setUseRiotDefaultRingtone +import im.vector.app.core.utils.RingtoneUtils import im.vector.app.features.analytics.plan.MobileScreen +import javax.inject.Inject +@AndroidEntryPoint class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { + @Inject lateinit var ringtoneUtils: RingtoneUtils + override var titleRes = R.string.preference_voice_and_video override val preferenceXmlRes = R.xml.vector_settings_voice_video @@ -52,12 +54,12 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { override fun bindPref() { // Incoming call sounds mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.let { setUseRiotDefaultRingtone(it, mUseRiotCallRingtonePreference.isChecked) } + ringtoneUtils.setUseRiotDefaultRingtone(mUseRiotCallRingtonePreference.isChecked) false } mCallRingtonePreference.let { - activity?.let { activity -> it.summary = getCallRingtoneName(activity) } + it.summary = ringtoneUtils.getCallRingtoneName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { displayRingtonePicker() false @@ -68,10 +70,9 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { private val ringtoneStartForActivityResult = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { val callRingtoneUri: Uri? = activityResult.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) - val thisActivity = activity - if (callRingtoneUri != null && thisActivity != null) { - setCallRingtoneUri(thisActivity, callRingtoneUri) - mCallRingtonePreference.summary = getCallRingtoneName(thisActivity) + if (callRingtoneUri != null) { + ringtoneUtils.setCallRingtoneUri(callRingtoneUri) + mCallRingtonePreference.summary = ringtoneUtils.getCallRingtoneName() } } } @@ -82,7 +83,7 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) - activity?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, getCallRingtoneUri(it)) } + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUtils.getCallRingtoneUri()) } ringtoneStartForActivityResult.launch(intent) } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index a652a62a6c..dcc584e6c3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -23,22 +23,15 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.auth.PendingAuthHandler import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.Matrix 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.failure.isInvalidUIAAuth import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import org.matrix.android.sdk.api.session.uia.exceptions.UiaCancelledException -import org.matrix.android.sdk.api.util.fromBase64 -import timber.log.Timber import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException data class DeactivateAccountViewState( val dummy: Boolean = false @@ -47,7 +40,7 @@ data class DeactivateAccountViewState( class DeactivateAccountViewModel @AssistedInject constructor( @Assisted private val initialState: DeactivateAccountViewState, private val session: Session, - private val matrix: Matrix, + private val pendingAuthHandler: PendingAuthHandler, ) : VectorViewModel(initialState) { @@ -56,39 +49,18 @@ class DeactivateAccountViewModel @AssistedInject constructor( override fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } - var uiaContinuation: Continuation? = null - var pendingAuth: UIABaseAuth? = null - override fun handle(action: DeactivateAccountAction) { when (action) { is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) DeactivateAccountAction.SsoAuthDone -> { - Timber.d("## UIA - FallBack success") _viewEvents.post(DeactivateAccountViewEvents.Loading()) - if (pendingAuth != null) { - uiaContinuation?.resume(pendingAuth!!) - } else { - uiaContinuation?.resumeWithException(IllegalArgumentException()) - } + pendingAuthHandler.ssoAuthDone() } is DeactivateAccountAction.PasswordAuthDone -> { _viewEvents.post(DeactivateAccountViewEvents.Loading()) - val decryptedPass = matrix.secureStorageService() - .loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) - uiaContinuation?.resume( - UserPasswordAuth( - session = pendingAuth?.session, - password = decryptedPass, - user = session.myUserId - ) - ) - } - DeactivateAccountAction.ReAuthCancelled -> { - Timber.d("## UIA - Reauth cancelled") - uiaContinuation?.resumeWithException(UiaCancelledException()) - uiaContinuation = null - pendingAuth = null + pendingAuthHandler.passwordAuthDone(action.password) } + DeactivateAccountAction.ReAuthCancelled -> pendingAuthHandler.reAuthCancelled() } } @@ -102,8 +74,8 @@ class DeactivateAccountViewModel @AssistedInject constructor( object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { _viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = flowResponse.session) + pendingAuthHandler.uiaContinuation = promise } } ) 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 87eaa01e2b..20b96e0029 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 @@ -24,12 +24,11 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth @@ -40,19 +39,17 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.util.awaitCallback -import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.flow.flow import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException class CrossSigningSettingsViewModel @AssistedInject constructor( @Assisted private val initialState: CrossSigningSettingsViewState, private val session: Session, private val reAuthHelper: ReAuthHelper, private val stringProvider: StringProvider, - private val matrix: Matrix, + private val pendingAuthHandler: PendingAuthHandler, ) : VectorViewModel(initialState) { init { @@ -77,9 +74,6 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( } } - var uiaContinuation: Continuation? = null - var pendingAuth: UIABaseAuth? = null - @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel @@ -110,8 +104,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( } else { Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = flowResponse.session) + pendingAuthHandler.uiaContinuation = promise } } }, it @@ -125,31 +119,11 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( } Unit } - is CrossSigningSettingsAction.SsoAuthDone -> { - Timber.d("## UIA - FallBack success") - if (pendingAuth != null) { - uiaContinuation?.resume(pendingAuth!!) - } else { - uiaContinuation?.resumeWithException(IllegalArgumentException()) - } - } - is CrossSigningSettingsAction.PasswordAuthDone -> { - val decryptedPass = matrix.secureStorageService() - .loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) - uiaContinuation?.resume( - UserPasswordAuth( - session = pendingAuth?.session, - password = decryptedPass, - user = session.myUserId - ) - ) - } + is CrossSigningSettingsAction.SsoAuthDone -> pendingAuthHandler.ssoAuthDone() + is CrossSigningSettingsAction.PasswordAuthDone -> pendingAuthHandler.passwordAuthDone(action.password) CrossSigningSettingsAction.ReAuthCancelled -> { - Timber.d("## UIA - Reauth cancelled") _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) - uiaContinuation?.resumeWithException(Exception()) - uiaContinuation = null - pendingAuth = null + pendingAuthHandler.reAuthCancelled() } } } 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 be19b0c227..0638b78586 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 @@ -32,10 +32,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.PublishDataSource -import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.login.ReAuthHelper -import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase +import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine @@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UIABaseAuth @@ -67,13 +66,11 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.util.awaitCallback -import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.flow.flow import timber.log.Timber import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException data class DevicesViewState( val myDeviceId: String = "", @@ -100,15 +97,12 @@ class DevicesViewModel @AssistedInject constructor( private val session: Session, private val reAuthHelper: ReAuthHelper, private val stringProvider: StringProvider, - private val matrix: Matrix, + private val pendingAuthHandler: PendingAuthHandler, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, ) : VectorViewModel(initialState), VerificationService.Listener { - var uiaContinuation: Continuation? = null - var pendingAuth: UIABaseAuth? = null - @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: DevicesViewState): DevicesViewModel @@ -234,37 +228,9 @@ class DevicesViewModel @AssistedInject constructor( is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) - is DevicesAction.SsoAuthDone -> { - // we should use token based auth - // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) - // will release the interactive auth interceptor - Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") - if (pendingAuth != null) { - uiaContinuation?.resume(pendingAuth!!) - } else { - uiaContinuation?.resumeWithException(IllegalArgumentException()) - } - Unit - } - is DevicesAction.PasswordAuthDone -> { - val decryptedPass = matrix.secureStorageService() - .loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) - uiaContinuation?.resume( - UserPasswordAuth( - session = pendingAuth?.session, - password = decryptedPass, - user = session.myUserId - ) - ) - Unit - } - DevicesAction.ReAuthCancelled -> { - Timber.d("## UIA - Reauth cancelled") -// _viewEvents.post(DevicesViewEvents.Loading) - uiaContinuation?.resumeWithException(Exception()) - uiaContinuation = null - pendingAuth = null - } + is DevicesAction.SsoAuthDone -> pendingAuthHandler.ssoAuthDone() + is DevicesAction.PasswordAuthDone -> pendingAuthHandler.passwordAuthDone(action.password) + DevicesAction.ReAuthCancelled -> pendingAuthHandler.reAuthCancelled() DevicesAction.ResetSecurity -> _viewEvents.post(DevicesViewEvents.PromptResetSecrets) } } @@ -373,8 +339,8 @@ class DevicesViewModel @AssistedInject constructor( } else { Timber.d("## UIA : deleteDevice UIA > start reauth activity") _viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = flowResponse.session) + pendingAuthHandler.uiaContinuation = promise } } }, it) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt index 8b58bd0536..a27c30379b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.devices import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo +import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import javax.inject.Inject class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor( 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 5b19b7a8d2..135c684e76 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 @@ -85,8 +85,7 @@ class VectorSettingsDevicesFragment : ).show(childFragmentManager, "REQPOP") } is DevicesViewEvents.SelfVerification -> { - VerificationBottomSheet.forSelfVerification(it.session) - .show(childFragmentManager, "REQPOP") + navigator.requestSelfSessionVerification(requireActivity()) } is DevicesViewEvents.ShowManuallyVerify -> { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceExtendedInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceExtendedInfo.kt new file mode 100644 index 0000000000..24e4606ca7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceExtendedInfo.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.features.settings.devices.v2.list.DeviceType + +data class DeviceExtendedInfo( + /** + * One of MOBILE, WEB, DESKTOP or UNKNOWN. + */ + val deviceType: DeviceType, + /** + * i.e. Google Pixel 6. + */ + val deviceModel: String? = null, + /** + * i.e. Android 11. + */ + val deviceOperatingSystem: String? = null, + /** + * i.e. Element Nightly. + */ + val clientName: String? = null, + /** + * i.e. 1.5.0. + */ + val clientVersion: String? = null, +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt index f0a91c6183..445eb6226f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt @@ -25,4 +25,6 @@ data class DeviceFullInfo( val cryptoDeviceInfo: CryptoDeviceInfo?, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel, val isInactive: Boolean, + val isCurrentDevice: Boolean, + val deviceExtendedInfo: DeviceExtendedInfo, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 8c7718bfcf..c7437db44c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -20,5 +20,6 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { + object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index e83004843d..c78c20f792 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo @@ -28,7 +27,7 @@ sealed class DevicesViewEvent : VectorViewEvents { data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent() - data class SelfVerification(val session: Session) : DevicesViewEvent() + object SelfVerification : DevicesViewEvent() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent() object PromptResetSecrets : DevicesViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index e0b6368fc1..a5405756eb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -24,26 +24,23 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.utils.PublishDataSource -import im.vector.lib.core.utils.flow.throttleFirst +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase +import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse -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 kotlin.time.Duration.Companion.seconds class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val activeSessionHolder: ActiveSessionHolder, + activeSessionHolder: ActiveSessionHolder, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, - private val refreshDevicesUseCase: RefreshDevicesUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, -) : VectorViewModel(initialState), VerificationService.Listener { + private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -52,35 +49,11 @@ class DevicesViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - private val refreshSource = PublishDataSource() - private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds - init { - addVerificationListener() observeCurrentSessionCrossSigningInfo() observeDevices() - observeRefreshSource() refreshDevicesOnCryptoDevicesChange() - queryRefreshDevicesList() - } - - override fun onCleared() { - removeVerificationListener() - super.onCleared() - } - - private fun addVerificationListener() { - activeSessionHolder.getSafeActiveSession() - ?.cryptoService() - ?.verificationService() - ?.addListener(this) - } - - private fun removeVerificationListener() { - activeSessionHolder.getSafeActiveSession() - ?.cryptoService() - ?.verificationService() - ?.removeListener(this) + refreshDeviceList() } private fun observeCurrentSessionCrossSigningInfo() { @@ -94,11 +67,14 @@ class DevicesViewModel @AssistedInject constructor( } private fun observeDevices() { - getDeviceFullInfoListUseCase.execute() + getDeviceFullInfoListUseCase.execute( + filterType = DeviceManagerFilterType.ALL_SESSIONS, + excludeCurrentDevice = false + ) .execute { async -> if (async is Success) { val deviceFullInfoList = async.invoke() - val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.isVerified.orFalse() } + val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } copy( devices = async, @@ -119,34 +95,24 @@ class DevicesViewModel @AssistedInject constructor( } } - private fun observeRefreshSource() { - refreshSource.stream() - .throttleFirst(refreshThrottleDelayMs) - .onEach { refreshDevicesUseCase.execute() } - .launchIn(viewModelScope) - } - - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx.state == VerificationTxState.Verified) { - queryRefreshDevicesList() - } - } - - /** - * Force the refresh of the devices list. - * The devices list is the list of the devices where the user is logged in. - * It can be any mobile devices, and any browsers. - */ - private fun queryRefreshDevicesList() { - refreshSource.post(Unit) - } - override fun handle(action: DevicesAction) { when (action) { + is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() } } + private fun handleVerifyCurrentSessionAction() { + viewModelScope.launch { + val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + if (currentSessionCanBeVerified) { + _viewEvents.post(DevicesViewEvent.SelfVerification) + } else { + _viewEvents.post(DevicesViewEvent.PromptResetSecrets) + } + } + } + private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt index 3fc061daa4..e8bed35e24 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2 import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo data class DevicesViewState( val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt index da2cf25f39..0272bea351 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt @@ -17,7 +17,12 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase +import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo +import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase +import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -32,16 +37,24 @@ class GetDeviceFullInfoListUseCase @Inject constructor( private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, + private val filterDevicesUseCase: FilterDevicesUseCase, + private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, ) { - fun execute(): Flow> { + fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow> { return activeSessionHolder.getSafeActiveSession()?.let { session -> val deviceFullInfoFlow = combine( getCurrentSessionCrossSigningInfoUseCase.execute(), session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() ) { currentSessionCrossSigningInfo, cryptoList, infoList -> - convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val excludedDeviceIds = if (excludeCurrentDevice) { + listOf(currentSessionCrossSigningInfo.deviceId) + } else { + emptyList() + } + filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds) } deviceFullInfoFlow.distinctUntilChanged() @@ -59,7 +72,9 @@ class GetDeviceFullInfoListUseCase @Inject constructor( val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) - DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive) + val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId + val deviceUserAgent = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent()) + DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt new file mode 100644 index 0000000000..f5f1782d82 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.features.settings.devices.v2.list.DeviceType +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class ParseDeviceUserAgentUseCase @Inject constructor() { + + fun execute(userAgent: String?): DeviceExtendedInfo { + if (userAgent == null) return createUnknownUserAgent() + + return when { + userAgent.contains(ANDROID_KEYWORD) -> parseAndroidUserAgent(userAgent) + userAgent.contains(IOS_KEYWORD) -> parseIosUserAgent(userAgent) + userAgent.contains(DESKTOP_KEYWORD) -> parseDesktopUserAgent(userAgent) + userAgent.contains(WEB_KEYWORD) -> parseWebUserAgent(userAgent) + else -> createUnknownUserAgent() + } + } + + private fun parseAndroidUserAgent(userAgent: String): DeviceExtendedInfo { + val appName = userAgent.substringBefore("/") + val appVersion = userAgent.substringAfter("/").substringBefore(" (") + val deviceInfoSegments = userAgent.substringAfter("(").substringBeforeLast(")").split("; ") + val deviceModel: String? + val deviceOperatingSystem: String? + if (deviceInfoSegments.firstOrNull() == "Linux") { + val deviceOperatingSystemIndex = deviceInfoSegments.indexOfFirst { it.startsWith("Android") } + deviceOperatingSystem = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex) + deviceModel = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex + 1) + } else { + deviceModel = deviceInfoSegments.getOrNull(0) + deviceOperatingSystem = deviceInfoSegments.getOrNull(1) + } + return DeviceExtendedInfo( + deviceType = DeviceType.MOBILE, + deviceModel = deviceModel, + deviceOperatingSystem = deviceOperatingSystem, + clientName = appName, + clientVersion = appVersion + ) + } + + private fun parseIosUserAgent(userAgent: String): DeviceExtendedInfo { + val appName = userAgent.substringBefore("/") + val appVersion = userAgent.substringAfter("/").substringBefore(" (") + val deviceInfoSegments = userAgent.substringAfter("(").substringBeforeLast(")").split("; ") + val deviceModel = deviceInfoSegments.getOrNull(0) + val deviceOperatingSystem = deviceInfoSegments.getOrNull(1) + return DeviceExtendedInfo( + deviceType = DeviceType.MOBILE, + deviceModel = deviceModel, + deviceOperatingSystem = deviceOperatingSystem, + clientName = appName, + clientVersion = appVersion + ) + } + + private fun parseDesktopUserAgent(userAgent: String): DeviceExtendedInfo { + val browserSegments = userAgent.split(" ") + val (browserName, browserVersion) = when { + isFirefox(browserSegments) -> { + Pair("Firefox", getBrowserVersion(browserSegments, "Firefox")) + } + isEdge(browserSegments) -> { + Pair("Edge", getBrowserVersion(browserSegments, "Edge")) + } + isMobile(browserSegments) -> { + when (val name = getMobileBrowserName(browserSegments)) { + null -> { + Pair(null, null) + } + "Safari" -> { + Pair(name, getBrowserVersion(browserSegments, "Version")) + } + else -> { + Pair(name, getBrowserVersion(browserSegments, name)) + } + } + } + isSafari(browserSegments) -> { + Pair("Safari", getBrowserVersion(browserSegments, "Version")) + } + else -> { + when (val name = getRegularBrowserName(browserSegments)) { + null -> { + Pair(null, null) + } + else -> { + Pair(name, getBrowserVersion(browserSegments, name)) + } + } + } + } + + val deviceOperatingSystemSegments = userAgent.substringAfter("(").substringBefore(")").split("; ") + val deviceOperatingSystem = if (deviceOperatingSystemSegments.getOrNull(1)?.startsWith("Android").orFalse()) { + deviceOperatingSystemSegments.getOrNull(1) + } else { + deviceOperatingSystemSegments.getOrNull(0) + } + return DeviceExtendedInfo( + deviceType = DeviceType.DESKTOP, + deviceModel = null, + deviceOperatingSystem = deviceOperatingSystem, + clientName = browserName, + clientVersion = browserVersion, + ) + } + + private fun parseWebUserAgent(userAgent: String): DeviceExtendedInfo { + return parseDesktopUserAgent(userAgent).copy( + deviceType = DeviceType.WEB + ) + } + + private fun createUnknownUserAgent(): DeviceExtendedInfo { + return DeviceExtendedInfo(DeviceType.UNKNOWN) + } + + private fun isFirefox(browserSegments: List): Boolean { + return browserSegments.lastOrNull()?.startsWith("Firefox").orFalse() + } + + private fun getBrowserVersion(browserSegments: List, browserName: String): String? { + // Chrome/104.0.3497.100 -> 104 + return browserSegments + .find { it.startsWith(browserName) } + ?.split("/") + ?.getOrNull(1) + ?.split(".") + ?.firstOrNull() + } + + private fun isEdge(browserSegments: List): Boolean { + return browserSegments.lastOrNull()?.startsWith("Edge").orFalse() + } + + private fun isSafari(browserSegments: List): Boolean { + return browserSegments.lastOrNull()?.startsWith("Safari").orFalse() && + browserSegments.getOrNull(browserSegments.size - 2)?.startsWith("Version").orFalse() + } + + private fun isMobile(browserSegments: List): Boolean { + return browserSegments.getOrNull(browserSegments.size - 2)?.startsWith("Mobile").orFalse() + } + + private fun getMobileBrowserName(browserSegments: List): String? { + val possibleBrowserName = browserSegments.getOrNull(browserSegments.size - 3)?.split("/")?.firstOrNull() + return if (possibleBrowserName == "Version") { + "Safari" + } else { + possibleBrowserName + } + } + + private fun getRegularBrowserName(browserSegments: List): String? { + return browserSegments.getOrNull(browserSegments.size - 2)?.split("/")?.firstOrNull() + } + + companion object { + // Element dbg/1.5.0-dev (Xiaomi; Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + // Legacy : Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) + private const val ANDROID_KEYWORD = "; MatrixAndroidSdk2" + + // Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00) + private const val IOS_KEYWORD = "; iOS " + + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 + // Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36 + private const val DESKTOP_KEYWORD = " Electron/" + + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 + private const val WEB_KEYWORD = "Mozilla/" + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt new file mode 100644 index 0000000000..938c6c99ab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import im.vector.app.R +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.databinding.ViewSessionWarningInfoBinding + +class SessionWarningInfoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewSessionWarningInfoBinding.inflate( + LayoutInflater.from(context), + this + ) + + var onLearnMoreClickListener: (() -> Unit)? = null + + init { + context.obtainStyledAttributes( + attrs, + R.styleable.SessionWarningInfoView, + 0, + 0 + ).use { + setDescription(it) + } + } + + private fun setDescription(typedArray: TypedArray) { + val description = typedArray.getString(R.styleable.SessionWarningInfoView_sessionsWarningInfoDescription) + val hasLearnMore = typedArray.getBoolean(R.styleable.SessionWarningInfoView_sessionsWarningInfoHasLearnMore, false) + if (hasLearnMore) { + val learnMore = context.getString(R.string.action_learn_more) + val fullDescription = buildString { + append(description) + append(" ") + append(learnMore) + } + + binding.sessionWarningInfoDescription.setTextWithColoredPart( + fullText = fullDescription, + coloredPart = learnMore, + underline = false + ) { + onLearnMoreClickListener?.invoke() + } + } else { + binding.sessionWarningInfoDescription.text = description + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt new file mode 100644 index 0000000000..8cb69a31ed --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSessionsListViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import com.airbnb.mvrx.MavericksState +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.utils.PublishDataSource +import im.vector.lib.core.utils.flow.throttleFirst +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +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 kotlin.time.Duration.Companion.seconds + +abstract class VectorSessionsListViewModel( + initialState: S, + private val activeSessionHolder: ActiveSessionHolder, + private val refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorViewModel(initialState), VerificationService.Listener { + + private val refreshSource = PublishDataSource() + private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds + + init { + addVerificationListener() + observeRefreshSource() + } + + override fun onCleared() { + removeVerificationListener() + super.onCleared() + } + + private fun addVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.addListener(this) + } + + private fun removeVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.removeListener(this) + } + + private fun observeRefreshSource() { + refreshSource.stream() + .throttleFirst(refreshThrottleDelayMs) + .onEach { refreshDevicesUseCase.execute() } + .launchIn(viewModelScope) + } + + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.state == VerificationTxState.Verified) { + refreshDeviceList() + } + } + + /** + * Force the refresh of the devices list. + * The devices list is the list of the devices where the user is logged in. + * It can be any mobile devices, and any browsers. + */ + fun refreshDeviceList() { + refreshSource.post(Unit) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index acf33dc01d..0fdbd40178 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.airbnb.mvrx.Success @@ -37,8 +36,11 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet -import im.vector.app.features.settings.devices.v2.list.OtherSessionsController +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER +import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import javax.inject.Inject @@ -48,7 +50,8 @@ import javax.inject.Inject */ @AndroidEntryPoint class VectorSettingsDevicesFragment : - VectorBaseFragment() { + VectorBaseFragment(), + OtherSessionsView.Callback { @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator @@ -78,9 +81,9 @@ class VectorSettingsDevicesFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initLearnMoreButtons() initWaitingView() initOtherSessionsView() + initSecurityRecommendationsView() observeViewEvents() } @@ -99,8 +102,7 @@ class VectorSettingsDevicesFragment : ).show(childFragmentManager, "REQPOP") } is DevicesViewEvent.SelfVerification -> { - VerificationBottomSheet.forSelfVerification(it.session) - .show(childFragmentManager, "REQPOP") + navigator.requestSelfSessionVerification(requireActivity()) } is DevicesViewEvent.ShowManuallyVerify -> { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { @@ -120,11 +122,30 @@ class VectorSettingsDevicesFragment : } private fun initOtherSessionsView() { - views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback { - override fun onItemClicked(deviceId: String) { - navigateToSessionOverview(deviceId) + views.deviceListOtherSessions.callback = this + } + + private fun initSecurityRecommendationsView() { + views.deviceListUnverifiedSessionsRecommendation.callback = object : SecurityRecommendationView.Callback { + override fun onViewAllClicked() { + viewNavigator.navigateToOtherSessions( + requireActivity(), + R.string.device_manager_header_section_security_recommendations_title, + DeviceManagerFilterType.UNVERIFIED, + excludeCurrentDevice = false + ) + } + } + views.deviceListInactiveSessionsRecommendation.callback = object : SecurityRecommendationView.Callback { + override fun onViewAllClicked() { + viewNavigator.navigateToOtherSessions( + requireActivity(), + R.string.device_manager_header_section_security_recommendations_title, + DeviceManagerFilterType.INACTIVE, + excludeCurrentDevice = false + ) } - }) + } } override fun onDestroyView() { @@ -132,12 +153,6 @@ class VectorSettingsDevicesFragment : super.onDestroyView() } - private fun initLearnMoreButtons() { - views.deviceListHeaderOtherSessions.onLearnMoreClickListener = { - Toast.makeText(context, "Learn more other", Toast.LENGTH_LONG).show() - } - } - private fun cleanUpLearnMoreButtonsListeners() { views.deviceListHeaderOtherSessions.onLearnMoreClickListener = null } @@ -201,7 +216,11 @@ class VectorSettingsDevicesFragment : } else { views.deviceListHeaderOtherSessions.isVisible = true views.deviceListOtherSessions.isVisible = true - views.deviceListOtherSessions.render(otherDevices) + views.deviceListOtherSessions.render( + devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), + totalNumberOfDevices = otherDevices.size, + showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER + ) } } @@ -225,6 +244,9 @@ class VectorSettingsDevicesFragment : views.deviceListCurrentSession.viewDetailsButton.debouncedClicks { currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } } + views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { + viewModel.handle(DevicesAction.VerifyCurrentSession) + } } ?: run { hideCurrentSessionView() } @@ -252,4 +274,17 @@ class VectorSettingsDevicesFragment : private fun handleLoadingStatus(isLoading: Boolean) { views.waitingView.root.isVisible = isLoading } + + override fun onOtherSessionClicked(deviceId: String) { + navigateToSessionOverview(deviceId) + } + + override fun onViewAllOtherSessionsClicked() { + viewNavigator.navigateToOtherSessions( + context = requireActivity(), + titleResourceId = R.string.device_manager_sessions_other_title, + defaultFilter = DeviceManagerFilterType.ALL_SESSIONS, + excludeCurrentDevice = true + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt index 54eed3bc14..47e697822b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt @@ -17,6 +17,8 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import javax.inject.Inject @@ -25,4 +27,15 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() { fun navigateToSessionOverview(context: Context, deviceId: String) { context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) } + + fun navigateToOtherSessions( + context: Context, + titleResourceId: Int, + defaultFilter: DeviceManagerFilterType, + excludeCurrentDevice: Boolean, + ) { + context.startActivity( + OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice) + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionDeviceIsVisibleUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionDeviceIsVisibleUseCase.kt new file mode 100644 index 0000000000..25b5ddb0e8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionDeviceIsVisibleUseCase.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import javax.inject.Inject + +class CheckIfSectionDeviceIsVisibleUseCase @Inject constructor() { + + fun execute(deviceInfo: DeviceInfo): Boolean { + return deviceInfo.lastSeenIp?.isNotEmpty().orFalse() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionSessionIsVisibleUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionSessionIsVisibleUseCase.kt new file mode 100644 index 0000000000..4998b4b5d3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/CheckIfSectionSessionIsVisibleUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import javax.inject.Inject + +class CheckIfSectionSessionIsVisibleUseCase @Inject constructor() { + + fun execute(deviceInfo: DeviceInfo): Boolean { + return deviceInfo.displayName?.isNotEmpty().orFalse() || + deviceInfo.deviceId?.isNotEmpty().orFalse() || + (deviceInfo.lastSeenTs ?: 0) > 0 + } +} diff --git a/vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsAction.kt similarity index 55% rename from vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsAction.kt index abee0cb2e7..0fa524dab4 100644 --- a/vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsAction.kt @@ -14,18 +14,10 @@ * limitations under the License. */ -package im.vector.app.core.di +package im.vector.app.features.settings.devices.v2.details -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager +import im.vector.app.core.platform.VectorViewModelAction -object DefaultSharedPreferences { - - @Volatile private var INSTANCE: SharedPreferences? = null - - fun getInstance(context: Context): SharedPreferences = - INSTANCE ?: synchronized(this) { - INSTANCE ?: PreferenceManager.getDefaultSharedPreferences(context.applicationContext).also { INSTANCE = it } - } +sealed class SessionDetailsAction : VectorViewModelAction { + data class CopyToClipboard(val content: String) : SessionDetailsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsActivity.kt new file mode 100644 index 0000000000..101bf1da2e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsActivity.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.SimpleFragmentActivity + +/** + * Display the details info about a Session. + */ +@AndroidEntryPoint +class SessionDetailsActivity : SimpleFragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + addFragment( + container = views.container, + fragmentClass = SessionDetailsFragment::class.java, + params = intent.getParcelableExtra(Mavericks.KEY_ARG) + ) + } + } + + companion object { + fun newIntent(context: Context, deviceId: String): Intent { + return Intent(context, SessionDetailsActivity::class.java).apply { + putExtra(Mavericks.KEY_ARG, SessionDetailsArgs(deviceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsArgs.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsArgs.kt new file mode 100644 index 0000000000..97716d7c47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsArgs.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SessionDetailsArgs( + val deviceId: String +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsContentItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsContentItem.kt new file mode 100644 index 0000000000..665ba5126c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsContentItem.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass +abstract class SessionDetailsContentItem : VectorEpoxyModel(R.layout.item_session_details_content) { + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute + var description: String? = null + + @EpoxyAttribute + var hasDivider: Boolean = true + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var onLongClickListener: View.OnLongClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.sessionDetailsContentTitle.text = title + holder.sessionDetailsContentDescription.text = description + holder.view.isClickable = onLongClickListener != null + holder.view.setOnLongClickListener(onLongClickListener) + holder.sessionDetailsContentDivider.isVisible = hasDivider + } + + class Holder : VectorEpoxyHolder() { + val sessionDetailsContentTitle by bind(R.id.sessionDetailsContentTitle) + val sessionDetailsContentDescription by bind(R.id.sessionDetailsContentDescription) + val sessionDetailsContentDivider by bind(R.id.sessionDetailsContentDivider) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt new file mode 100644 index 0000000000..1fb5be4d78 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsController.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.view.View +import androidx.annotation.StringRes +import com.airbnb.epoxy.TypedEpoxyController +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.StringProvider +import im.vector.app.core.utils.DimensionConverter +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import javax.inject.Inject + +class SessionDetailsController @Inject constructor( + private val checkIfSectionSessionIsVisibleUseCase: CheckIfSectionSessionIsVisibleUseCase, + private val checkIfSectionDeviceIsVisibleUseCase: CheckIfSectionDeviceIsVisibleUseCase, + private val stringProvider: StringProvider, + private val dateFormatter: VectorDateFormatter, + private val dimensionConverter: DimensionConverter, +) : TypedEpoxyController() { + + var callback: Callback? = null + + interface Callback { + fun onItemLongClicked(content: String) + } + + override fun buildModels(data: DeviceInfo?) { + data?.let { info -> + val hasSectionSession = hasSectionSession(data) + if (hasSectionSession) { + buildSectionSession(info) + } + + if (hasSectionDevice(data)) { + buildSectionDevice(info, addExtraTopMargin = hasSectionSession) + } + } + } + + private fun buildHeaderItem(@StringRes titleResId: Int, addExtraTopMargin: Boolean = false) { + val host = this + sessionDetailsHeaderItem { + id(titleResId) + title(host.stringProvider.getString(titleResId)) + addExtraTopMargin(addExtraTopMargin) + dimensionConverter(host.dimensionConverter) + } + } + + private fun buildContentItem(@StringRes titleResId: Int, value: String, hasDivider: Boolean) { + val host = this + sessionDetailsContentItem { + id(titleResId) + title(host.stringProvider.getString(titleResId)) + description(value) + hasDivider(hasDivider) + onLongClickListener(View.OnLongClickListener { + host.callback?.onItemLongClicked(value) + true + }) + } + } + + private fun hasSectionSession(data: DeviceInfo): Boolean { + return checkIfSectionSessionIsVisibleUseCase.execute(data) + } + + private fun buildSectionSession(data: DeviceInfo) { + val sessionName = data.displayName + val sessionId = data.deviceId + val sessionLastSeenTs = data.lastSeenTs + + buildHeaderItem(R.string.device_manager_session_title) + + sessionName?.let { + val hasDivider = sessionId != null || sessionLastSeenTs != null + buildContentItem(R.string.device_manager_session_details_session_name, it, hasDivider) + } + sessionId?.let { + val hasDivider = sessionLastSeenTs != null + buildContentItem(R.string.device_manager_session_details_session_id, it, hasDivider) + } + sessionLastSeenTs?.let { + val formattedDate = dateFormatter.format(it, DateFormatKind.MESSAGE_DETAIL) + val hasDivider = false + buildContentItem(R.string.device_manager_session_details_session_last_activity, formattedDate, hasDivider) + } + } + + private fun hasSectionDevice(data: DeviceInfo): Boolean { + return checkIfSectionDeviceIsVisibleUseCase.execute(data) + } + + private fun buildSectionDevice(data: DeviceInfo, addExtraTopMargin: Boolean) { + val lastSeenIp = data.lastSeenIp + + buildHeaderItem(R.string.device_manager_device_title, addExtraTopMargin) + + lastSeenIp?.let { + val hasDivider = false + buildContentItem(R.string.device_manager_session_details_device_ip_address, it, hasDivider) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsFragment.kt new file mode 100644 index 0000000000..5d7717e5f7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsFragment.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +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.platform.showOptimizedSnackbar +import im.vector.app.databinding.FragmentSessionDetailsBinding +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import javax.inject.Inject + +/** + * Display the details info about a Session. + */ +@AndroidEntryPoint +class SessionDetailsFragment : + VectorBaseFragment() { + + @Inject lateinit var sessionDetailsController: SessionDetailsController + + private val viewModel: SessionDetailsViewModel by fragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionDetailsBinding { + return FragmentSessionDetailsBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initToolbar() + initSessionDetails() + observeViewEvents() + } + + private fun initToolbar() { + (activity as? AppCompatActivity) + ?.supportActionBar + ?.setTitle(R.string.device_manager_session_details_title) + } + + private fun initSessionDetails() { + sessionDetailsController.callback = object : SessionDetailsController.Callback { + override fun onItemLongClicked(content: String) { + viewModel.handle(SessionDetailsAction.CopyToClipboard(content)) + } + } + views.sessionDetails.configureWith(sessionDetailsController) + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { viewEvent -> + when (viewEvent) { + SessionDetailsViewEvent.ContentCopiedToClipboard -> view?.showOptimizedSnackbar(getString(R.string.copied_to_clipboard)) + } + } + } + + override fun onDestroyView() { + cleanUpSessionDetails() + super.onDestroyView() + } + + private fun cleanUpSessionDetails() { + sessionDetailsController.callback = null + views.sessionDetails.cleanup() + } + + override fun invalidate() = withState(viewModel) { state -> + if (state.deviceInfo is Success) { + renderSessionDetails(state.deviceInfo.invoke()) + } else { + hideSessionDetails() + } + } + + private fun renderSessionDetails(deviceInfo: DeviceInfo) { + views.sessionDetails.isVisible = true + sessionDetailsController.setData(deviceInfo) + } + + private fun hideSessionDetails() { + views.sessionDetails.isGone = true + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt new file mode 100644 index 0000000000..ff6ce3faad --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.updateLayoutParams +import androidx.core.view.updateMargins +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.utils.DimensionConverter + +private const val EXTRA_TOP_MARGIN_DP = 48 + +@EpoxyModelClass +abstract class SessionDetailsHeaderItem : VectorEpoxyModel(R.layout.item_session_details_header) { + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute + var addExtraTopMargin: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var dimensionConverter: DimensionConverter? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.sessionDetailsHeaderTitle.text = title + val topMargin = if (addExtraTopMargin) { + dimensionConverter?.dpToPx(EXTRA_TOP_MARGIN_DP) ?: 0 + } else { + 0 + } + holder.sessionDetailsHeaderTitle.updateLayoutParams { + updateMargins(top = topMargin) + } + } + + class Holder : VectorEpoxyHolder() { + val sessionDetailsHeaderTitle by bind(R.id.sessionDetailsHeaderTitle) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewEvent.kt new file mode 100644 index 0000000000..02b313319e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SessionDetailsViewEvent : VectorViewEvents { + object ContentCopiedToClipboard : SessionDetailsViewEvent() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewModel.kt new file mode 100644 index 0000000000..c37858cc54 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.utils.CopyToClipboardUseCase +import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class SessionDetailsViewModel @AssistedInject constructor( + @Assisted val initialState: SessionDetailsViewState, + private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, + private val copyToClipboardUseCase: CopyToClipboardUseCase, +) : VectorViewModel(initialState) { + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SessionDetailsViewState): SessionDetailsViewModel + } + + init { + observeSessionInfo(initialState.deviceId) + } + + private fun observeSessionInfo(deviceId: String) { + getDeviceFullInfoUseCase.execute(deviceId) + .onEach { setState { copy(deviceInfo = Success(it.deviceInfo)) } } + .launchIn(viewModelScope) + } + + override fun handle(action: SessionDetailsAction) { + return when (action) { + is SessionDetailsAction.CopyToClipboard -> handleCopyToClipboard(action) + } + } + + private fun handleCopyToClipboard(copyToClipboard: SessionDetailsAction.CopyToClipboard) { + copyToClipboardUseCase.execute(copyToClipboard.content) + _viewEvents.post(SessionDetailsViewEvent.ContentCopiedToClipboard) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewState.kt new file mode 100644 index 0000000000..15868d3110 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsViewState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.details + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo + +data class SessionDetailsViewState( + val deviceId: String, + val deviceInfo: Async = Uninitialized, +) : MavericksState { + constructor(args: SessionDetailsArgs) : this( + deviceId = args.deviceId + ) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt new file mode 100644 index 0000000000..28c7045a82 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK +import im.vector.app.databinding.BottomSheetDeviceManagerFilterBinding +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DeviceManagerFilterBottomSheetArgs( + val initialFilterType: DeviceManagerFilterType, +) : Parcelable + +@AndroidEntryPoint +class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val args: DeviceManagerFilterBottomSheetArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetDeviceManagerFilterBinding { + return BottomSheetDeviceManagerFilterBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initFilterRadioGroup() + } + + private fun initFilterRadioGroup() { + views.filterOptionInactiveTextView.text = resources.getQuantityString( + R.plurals.device_manager_filter_option_inactive_description, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + ) + + val radioButtonId = when (args.initialFilterType) { + DeviceManagerFilterType.ALL_SESSIONS -> R.id.filterOptionAllSessionsRadioButton + DeviceManagerFilterType.VERIFIED -> R.id.filterOptionVerifiedRadioButton + DeviceManagerFilterType.UNVERIFIED -> R.id.filterOptionUnverifiedRadioButton + DeviceManagerFilterType.INACTIVE -> R.id.filterOptionInactiveRadioButton + } + views.filterOptionsRadioGroup.check(radioButtonId) + + views.filterOptionVerifiedTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionVerifiedRadioButton) + } + views.filterOptionUnverifiedTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionUnverifiedRadioButton) + } + views.filterOptionInactiveTextView.debouncedClicks { + views.filterOptionsRadioGroup.check(R.id.filterOptionInactiveRadioButton) + } + + views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId -> + onFilterTypeChanged(checkedId) + } + } + + private fun onFilterTypeChanged(checkedId: Int) { + val filterType = when (checkedId) { + R.id.filterOptionAllSessionsRadioButton -> DeviceManagerFilterType.ALL_SESSIONS + R.id.filterOptionVerifiedRadioButton -> DeviceManagerFilterType.VERIFIED + R.id.filterOptionUnverifiedRadioButton -> DeviceManagerFilterType.UNVERIFIED + R.id.filterOptionInactiveRadioButton -> DeviceManagerFilterType.INACTIVE + else -> DeviceManagerFilterType.ALL_SESSIONS + } + resultListener?.onBottomSheetResult(RESULT_OK, filterType) + dismiss() + } + + companion object { + fun newInstance(initialFilterType: DeviceManagerFilterType, resultListener: ResultListener): DeviceManagerFilterBottomSheet { + return DeviceManagerFilterBottomSheet().apply { + this.resultListener = resultListener + setArguments(DeviceManagerFilterBottomSheetArgs(initialFilterType)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt new file mode 100644 index 0000000000..a1ef08f7df --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterType.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +enum class DeviceManagerFilterType { + ALL_SESSIONS, + VERIFIED, + UNVERIFIED, + INACTIVE, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt new file mode 100644 index 0000000000..a23a7a7108 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.filter + +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class FilterDevicesUseCase @Inject constructor() { + + fun execute( + devices: List, + filterType: DeviceManagerFilterType, + excludedDeviceIds: List = emptyList(), + ): List { + return devices + .filter { + when (filterType) { + DeviceManagerFilterType.ALL_SESSIONS -> true + DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() + DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() + DeviceManagerFilterType.INACTIVE -> it.isInactive + } + } + .filter { it.deviceInfo.deviceId !in excludedDeviceIds } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt index c73389d775..283e64fffe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.list import android.graphics.drawable.Drawable import android.widget.ImageView import android.widget.TextView +import androidx.annotation.ColorInt import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -45,6 +46,10 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la @EpoxyAttribute var sessionDescription: String? = null + @EpoxyAttribute + @ColorInt + var sessionDescriptionColor: Int? = null + @EpoxyAttribute var sessionDescriptionDrawable: Drawable? = null @@ -82,6 +87,9 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) holder.otherSessionNameTextView.text = sessionName holder.otherSessionDescriptionTextView.text = sessionDescription + sessionDescriptionColor?.let { + holder.otherSessionDescriptionTextView.setTextColor(it) + } holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt index 468b19c45a..afa640fb9a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -50,23 +50,17 @@ class OtherSessionsController @Inject constructor( text(host.stringProvider.getString(R.string.no_result_placeholder)) } } else { - data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device -> + data.forEach { device -> val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind) - val description = if (device.isInactive) { - stringProvider.getQuantityString( - R.plurals.device_manager_other_sessions_description_inactive, - SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, - SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, - formattedLastActivityDate - ) - } else if (device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) { - stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate) + val description = calculateDescription(device, formattedLastActivityDate) + val descriptionColor = if (device.isCurrentDevice) { + host.colorProvider.getColorFromAttribute(R.attr.colorError) } else { - stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate) + host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) } - val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val descriptionDrawable = if (device.isInactive) drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null + val drawableColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val descriptionDrawable = if (device.isInactive) host.drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null otherSessionItem { id(device.deviceInfo.deviceId) @@ -75,10 +69,33 @@ class OtherSessionsController @Inject constructor( sessionName(device.deviceInfo.displayName) sessionDescription(description) sessionDescriptionDrawable(descriptionDrawable) + sessionDescriptionColor(descriptionColor) stringProvider(this@OtherSessionsController.stringProvider) clickListener { device.deviceInfo.deviceId?.let { host.callback?.onItemClicked(it) } } } } } } + + private fun calculateDescription(device: DeviceFullInfo, formattedLastActivityDate: String): String { + return when { + device.isInactive -> { + stringProvider.getQuantityString( + R.plurals.device_manager_other_sessions_description_inactive, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + formattedLastActivityDate + ) + } + device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> { + stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate) + } + device.isCurrentDevice -> { + stringProvider.getString(R.string.device_manager_other_sessions_description_unverified_current_session) + } + else -> { + stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt index b552664fe9..6f6956c885 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt @@ -19,8 +19,13 @@ package im.vector.app.features.settings.devices.v2.list import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.OnModelBuildFinishedListener import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.databinding.ViewOtherSessionsBinding @@ -32,30 +37,74 @@ class OtherSessionsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback { + + interface Callback { + fun onOtherSessionClicked(deviceId: String) + fun onViewAllOtherSessionsClicked() + } @Inject lateinit var otherSessionsController: OtherSessionsController private val views: ViewOtherSessionsBinding + private lateinit var recyclerViewDataObserver: RecyclerView.AdapterDataObserver + private lateinit var stateRestorer: LayoutManagerStateRestorer + private var modelBuildListener: OnModelBuildFinishedListener? = null + + var callback: Callback? = null init { inflate(context, R.layout.view_other_sessions, this) views = ViewOtherSessionsBinding.bind(this) + + configureOtherSessionsRecyclerView() + + views.otherSessionsViewAllButton.setOnClickListener { + callback?.onViewAllOtherSessionsClicked() + } } - fun render(devices: List) { - views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true) - views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, devices.size) - otherSessionsController.setData(devices) + private fun configureOtherSessionsRecyclerView() { + views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = false) + + val layoutManager = LinearLayoutManager(context) + stateRestorer = LayoutManagerStateRestorer(layoutManager) + views.otherSessionsRecyclerView.layoutManager = layoutManager + layoutManager.recycleChildrenOnDetach = true + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } + otherSessionsController.addModelBuildListener(modelBuildListener) + + recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + views.otherSessionsRecyclerView.scrollToPosition(0) + } + } + otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver) + + otherSessionsController.callback = this } - fun setCallback(callback: OtherSessionsController.Callback) { - otherSessionsController.callback = callback + fun render(devices: List, totalNumberOfDevices: Int, showViewAll: Boolean) { + if (showViewAll) { + views.otherSessionsViewAllButton.isVisible = true + views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices) + } else { + views.otherSessionsViewAllButton.isVisible = false + } + otherSessionsController.setData(devices) } override fun onDetachedFromWindow() { + otherSessionsController.removeModelBuildListener(modelBuildListener) + modelBuildListener = null otherSessionsController.callback = null + otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver) views.otherSessionsRecyclerView.cleanup() super.onDetachedFromWindow() } + + override fun onItemClicked(deviceId: String) { + callback?.onOtherSessionClicked(deviceId) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt index 93cf3c0501..07202274ad 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt @@ -31,7 +31,12 @@ class SecurityRecommendationView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { + interface Callback { + fun onViewAllClicked() + } + private val views: ViewSecurityRecommendationBinding + var callback: Callback? = null init { inflate(context, R.layout.view_security_recommendation, this) @@ -47,6 +52,10 @@ class SecurityRecommendationView @JvmOverloads constructor( setDescription(it) setImage(it) } + + views.recommendationViewAllButton.setOnClickListener { + callback?.onViewAllClicked() + } } private fun setTitle(typedArray: TypedArray) { @@ -78,4 +87,9 @@ class SecurityRecommendationView @JvmOverloads constructor( setDescription(viewState.description) setCount(viewState.sessionsCount) } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + callback = null + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 0cb621a502..340a4f3c3a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider @@ -48,6 +49,7 @@ class SessionInfoView @JvmOverloads constructor( } val viewDetailsButton = views.sessionInfoViewDetailsButton + val viewVerifyButton = views.sessionInfoVerifySessionButton fun render( sessionInfoViewState: SessionInfoViewState, @@ -60,6 +62,7 @@ class SessionInfoView @JvmOverloads constructor( sessionInfoViewState.deviceFullInfo.roomEncryptionTrustLevel, sessionInfoViewState.isCurrentSession, sessionInfoViewState.isLearnMoreLinkVisible, + sessionInfoViewState.isVerifyButtonVisible, ) renderDeviceLastSeenDetails( sessionInfoViewState.deviceFullInfo.isInactive, @@ -76,12 +79,13 @@ class SessionInfoView @JvmOverloads constructor( encryptionTrustLevel: RoomEncryptionTrustLevel, isCurrentSession: Boolean, hasLearnMoreLink: Boolean, + isVerifyButtonVisible: Boolean, ) { views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel) if (encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) { renderCrossSigningVerified(isCurrentSession) } else { - renderCrossSigningUnverified(isCurrentSession) + renderCrossSigningUnverified(isCurrentSession, isVerifyButtonVisible) } if (hasLearnMoreLink) { appendLearnMoreToVerificationStatus() @@ -91,13 +95,14 @@ class SessionInfoView @JvmOverloads constructor( private fun appendLearnMoreToVerificationStatus() { val status = views.sessionInfoVerificationStatusDetailTextView.text val learnMore = context.getString(R.string.action_learn_more) - val stringBuilder = StringBuilder() - stringBuilder.append(status) - stringBuilder.append(" ") - stringBuilder.append(learnMore) + val statusText = buildString { + append(status) + append(" ") + append(learnMore) + } views.sessionInfoVerificationStatusDetailTextView.setTextWithColoredPart( - fullText = stringBuilder.toString(), + fullText = statusText, coloredPart = learnMore, underline = false ) { @@ -117,7 +122,7 @@ class SessionInfoView @JvmOverloads constructor( views.sessionInfoVerifySessionButton.isVisible = false } - private fun renderCrossSigningUnverified(isCurrentSession: Boolean) { + private fun renderCrossSigningUnverified(isCurrentSession: Boolean, isVerifyButtonVisible: Boolean) { views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified) views.sessionInfoVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError)) val statusResId = if (isCurrentSession) { @@ -126,7 +131,7 @@ class SessionInfoView @JvmOverloads constructor( R.string.device_manager_verification_status_detail_other_session_unverified } views.sessionInfoVerificationStatusDetailTextView.text = context.getString(statusResId) - views.sessionInfoVerifySessionButton.isVisible = true + views.sessionInfoVerifySessionButton.isVisible = isVerifyButtonVisible } // TODO. We don't have this info yet. Update later accordingly. @@ -172,15 +177,7 @@ class SessionInfoView @JvmOverloads constructor( views.sessionInfoLastActivityTextView.isGone = true } - deviceInfo.lastSeenIp - ?.takeIf { isLastSeenDetailsVisible } - ?.let { ipAddress -> - views.sessionInfoLastIPAddressTextView.isVisible = true - views.sessionInfoLastIPAddressTextView.text = ipAddress - } - ?: run { - views.sessionInfoLastIPAddressTextView.isGone = true - } + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 60e1234820..287bb956f5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -21,6 +21,7 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo data class SessionInfoViewState( val isCurrentSession: Boolean, val deviceFullInfo: DeviceFullInfo, + val isVerifyButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, val isLastSeenDetailsVisible: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 547ed93f24..0660e7d642 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.databinding.ViewSessionsListHeaderBinding @@ -53,26 +54,36 @@ class SessionsListHeaderView @JvmOverloads constructor( } private fun setTitle(typedArray: TypedArray) { - val title = typedArray.getString(R.styleable.SessionsListHeaderView_devicesListHeaderTitle) - binding.sessionsListHeaderTitle.text = title + val title = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderTitle) + binding.sessionsListHeaderTitle.setTextOrHide(title) } private fun setDescription(typedArray: TypedArray) { - val description = typedArray.getString(R.styleable.SessionsListHeaderView_devicesListHeaderDescription) + val description = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderDescription) if (description.isNullOrEmpty()) { binding.sessionsListHeaderDescription.isVisible = false return } - val learnMore = context.getString(R.string.action_learn_more) - val stringBuilder = StringBuilder() - stringBuilder.append(description) - stringBuilder.append(" ") - stringBuilder.append(learnMore) + val hasLearnMoreLink = typedArray.getBoolean(R.styleable.SessionsListHeaderView_sessionsListHeaderHasLearnMoreLink, true) + if (hasLearnMoreLink) { + setDescriptionWithLearnMore(description) + } else { + binding.sessionsListHeaderDescription.text = description + } binding.sessionsListHeaderDescription.isVisible = true + } + + private fun setDescriptionWithLearnMore(description: String) { + val learnMore = context.getString(R.string.action_learn_more) + val fullDescription = buildString { + append(description) + append(" ") + append(learnMore) + } binding.sessionsListHeaderDescription.setTextWithColoredPart( - fullText = stringBuilder.toString(), + fullText = fullDescription, coloredPart = learnMore, underline = false ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt new file mode 100644 index 0000000000..22ca06eb1e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.more + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetSessionLearnMoreBinding +import kotlinx.parcelize.Parcelize + +@AndroidEntryPoint +class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class Args( + val title: String, + val description: String, + ) : Parcelable + + private val viewModel: SessionLearnMoreViewModel by fragmentViewModel() + + override val showExpanded = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { + return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initCloseButton() + } + + private fun initCloseButton() { + views.bottomSheetSessionLearnMoreCloseButton.debouncedClicks { + dismiss() + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + super.invalidate() + views.bottomSheetSessionLearnMoreTitle.text = viewState.title + views.bottomSheetSessionLearnMoreDescription.text = viewState.description + } + + companion object { + + fun show(fragmentManager: FragmentManager, args: Args) { + val bottomSheet = SessionLearnMoreBottomSheet() + bottomSheet.isCancelable = true + bottomSheet.setArguments(args) + bottomSheet.show(fragmentManager, "SessionLearnMoreBottomSheet") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt new file mode 100644 index 0000000000..09ca2df15d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.more + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel + +class SessionLearnMoreViewModel @AssistedInject constructor( + @Assisted initialState: SessionLearnMoreViewState, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SessionLearnMoreViewState): SessionLearnMoreViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: EmptyAction) { + // do nothing + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt new file mode 100644 index 0000000000..cade2ce861 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.more + +import com.airbnb.mvrx.MavericksState + +data class SessionLearnMoreViewState( + val title: String, + val description: String, +) : MavericksState { + constructor(args: SessionLearnMoreBottomSheet.Args) : this( + title = args.title, + description = args.description, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt new file mode 100644 index 0000000000..7164ecc866 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +sealed class OtherSessionsAction : VectorViewModelAction { + data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt new file mode 100644 index 0000000000..f146f77690 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsActivity.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.annotation.StringRes +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +@AndroidEntryPoint +class OtherSessionsActivity : SimpleFragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + views.toolbar.visibility = View.GONE + + if (isFirstCreation()) { + addFragment( + container = views.container, + fragmentClass = OtherSessionsFragment::class.java, + params = intent.getParcelableExtra(Mavericks.KEY_ARG) + ) + } + } + + companion object { + fun newIntent( + context: Context, + @StringRes + titleResourceId: Int, + defaultFilter: DeviceManagerFilterType, + excludeCurrentDevice: Boolean, + ): Intent { + return Intent(context, OtherSessionsActivity::class.java).apply { + putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(titleResourceId, defaultFilter, excludeCurrentDevice)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsArgs.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsArgs.kt new file mode 100644 index 0000000000..61f89eaffa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsArgs.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.os.Parcelable +import androidx.annotation.StringRes +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import kotlinx.parcelize.Parcelize + +@Parcelize +data class OtherSessionsArgs( + @StringRes + val titleResourceId: Int, + val defaultFilter: DeviceManagerFilterType, + val excludeCurrentDevice: Boolean, +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt new file mode 100644 index 0000000000..610776e22e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentOtherSessionsBinding +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.list.OtherSessionsView +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.themes.ThemeUtils +import javax.inject.Inject + +@AndroidEntryPoint +class OtherSessionsFragment : + VectorBaseFragment(), + VectorBaseBottomSheetDialogFragment.ResultListener, + OtherSessionsView.Callback { + + private val viewModel: OtherSessionsViewModel by fragmentViewModel() + private val args: OtherSessionsArgs by args() + + @Inject lateinit var colorProvider: ColorProvider + + @Inject lateinit var viewNavigator: OtherSessionsViewNavigator + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { + return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.otherSessionsToolbar).setTitle(args.titleResourceId).allowBack() + observeViewEvents() + initFilterView() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is OtherSessionsViewEvents.Loading -> showLoading(it.message) + is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + } + } + } + + private fun initFilterView() { + views.otherSessionsFilterFrameLayout.debouncedClicks { + withState(viewModel) { state -> + DeviceManagerFilterBottomSheet + .newInstance(state.currentFilter, this) + .show(requireActivity().supportFragmentManager, "SHOW_DEVICE_MANAGER_FILTER_BOTTOM_SHEET") + } + } + + views.otherSessionsClearFilterButton.debouncedClicks { + viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS)) + } + + views.deviceListOtherSessions.callback = this + + if (args.defaultFilter != DeviceManagerFilterType.ALL_SESSIONS) { + viewModel.handle(OtherSessionsAction.FilterDevices(args.defaultFilter)) + } + } + + override fun onBottomSheetResult(resultCode: Int, data: Any?) { + if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) { + viewModel.handle(OtherSessionsAction.FilterDevices(data)) + } + } + + override fun invalidate() = withState(viewModel) { state -> + if (state.devices is Success) { + renderDevices(state.devices(), state.currentFilter) + } + } + + private fun renderDevices(devices: List?, currentFilter: DeviceManagerFilterType) { + views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS + + when (currentFilter) { + DeviceManagerFilterType.VERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_verified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_verified), + imageResourceId = R.drawable.ic_shield_trusted_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_trust_background) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found) + updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified) + } + DeviceManagerFilterType.UNVERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_unverified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_unverified), + imageResourceId = R.drawable.ic_shield_warning_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_warning_background) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_unverified_sessions_found) + updateSecurityLearnMoreButton( + R.string.device_manager_learn_more_sessions_unverified_title, + R.string.device_manager_learn_more_sessions_unverified + ) + } + DeviceManagerFilterType.INACTIVE -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_inactive), + description = resources.getQuantityString( + R.plurals.device_manager_other_sessions_recommendation_description_inactive, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + ), + imageResourceId = R.drawable.ic_inactive_sessions, + imageTintColorResourceId = ThemeUtils.getColor(requireContext(), R.attr.vctr_system) + ) + ) + views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found) + updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_inactive_title, R.string.device_manager_learn_more_sessions_inactive) + } + DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ + } + } + + if (devices.isNullOrEmpty()) { + views.deviceListOtherSessions.isVisible = false + views.otherSessionsNotFoundLayout.isVisible = true + } else { + views.deviceListOtherSessions.isVisible = true + views.otherSessionsNotFoundLayout.isVisible = false + views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false) + } + } + + private fun updateSecurityLearnMoreButton( + @StringRes titleResId: Int, + @StringRes descriptionResId: Int, + ) { + views.otherSessionsSecurityRecommendationView.onLearnMoreClickListener = { + showLearnMoreInfo(titleResId, getString(descriptionResId)) + } + } + + private fun showLearnMoreInfo( + @StringRes titleResId: Int, + description: String, + ) { + val args = SessionLearnMoreBottomSheet.Args( + title = getString(titleResId), + description = description, + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) + } + + override fun onOtherSessionClicked(deviceId: String) { + viewNavigator.navigateToSessionOverview( + context = requireActivity(), + deviceId = deviceId + ) + } + + override fun onViewAllOtherSessionsClicked() { + // NOOP. We don't have this button in this screen + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt new file mode 100644 index 0000000000..5a7d1fa910 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.TypedArray +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding + +@AndroidEntryPoint +class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val views: ViewOtherSessionSecurityRecommendationBinding + var onLearnMoreClickListener: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_other_session_security_recommendation, this) + views = ViewOtherSessionSecurityRecommendationBinding.bind(this) + + context.obtainStyledAttributes( + attrs, + R.styleable.OtherSessionsSecurityRecommendationView, + 0, + 0 + ).use { + setTitle(it) + setDescription(it) + setImage(it) + } + } + + private fun setTitle(typedArray: TypedArray) { + val title = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationTitle) + setTitle(title) + } + + private fun setTitle(title: String?) { + views.recommendationTitleTextView.text = title + } + + private fun setDescription(typedArray: TypedArray) { + val description = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationDescription) + setDescription(description) + } + + private fun setImage(typedArray: TypedArray) { + val imageResource = typedArray.getResourceId(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageResource, 0) + val backgroundTint = typedArray.getColor(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageBackgroundTint, 0) + setImageResource(imageResource) + setImageBackgroundTint(backgroundTint) + } + + private fun setImageResource(resourceId: Int) { + views.recommendationShieldImageView.setImageResource(resourceId) + } + + private fun setImageBackgroundTint(backgroundTintColor: Int) { + views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor) + } + + private fun setDescription(description: String?) { + val learnMore = context.getString(R.string.action_learn_more) + val formattedDescription = buildString { + append(description) + append(" ") + append(learnMore) + } + + views.recommendationDescriptionTextView.setTextWithColoredPart( + fullText = formattedDescription, + coloredPart = learnMore, + underline = false + ) { + onLearnMoreClickListener?.invoke() + } + } + + fun render(viewState: OtherSessionsSecurityRecommendationViewState) { + setTitle(viewState.title) + setDescription(viewState.description) + setImageResource(viewState.imageResourceId) + setImageBackgroundTint(viewState.imageTintColorResourceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt new file mode 100644 index 0000000000..2b17cb26b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +data class OtherSessionsSecurityRecommendationViewState( + val title: String, + val description: String, + val imageResourceId: Int, + val imageTintColorResourceId: Int, +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt new file mode 100644 index 0000000000..95f9c72b33 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewEvents + +sealed class OtherSessionsViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() + data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt new file mode 100644 index 0000000000..e52953e2b6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase +import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import kotlinx.coroutines.Job + +class OtherSessionsViewModel @AssistedInject constructor( + @Assisted private val initialState: OtherSessionsViewState, + activeSessionHolder: ActiveSessionHolder, + private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + refreshDevicesUseCase: RefreshDevicesUseCase +) : VectorSessionsListViewModel( + initialState, activeSessionHolder, refreshDevicesUseCase +) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + private var observeDevicesJob: Job? = null + + init { + observeDevices(initialState.currentFilter) + } + + private fun observeDevices(currentFilter: DeviceManagerFilterType) { + observeDevicesJob?.cancel() + observeDevicesJob = getDeviceFullInfoListUseCase.execute( + filterType = currentFilter, + excludeCurrentDevice = initialState.excludeCurrentDevice + ) + .execute { async -> + copy( + devices = async, + ) + } + } + + override fun handle(action: OtherSessionsAction) { + when (action) { + is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) + } + } + + private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { + setState { + copy( + currentFilter = action.filterType + ) + } + observeDevices(action.filterType) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt new file mode 100644 index 0000000000..ef1895d0ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import android.content.Context +import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity +import javax.inject.Inject + +class OtherSessionsViewNavigator @Inject constructor() { + + fun navigateToSessionOverview(context: Context, deviceId: String) { + context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt new file mode 100644 index 0000000000..5256a9b27a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +data class OtherSessionsViewState( + val devices: Async> = Uninitialized, + val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, + val excludeCurrentDevice: Boolean = false, +) : MavericksState { + + constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt index fff81b6dc5..42cd49b072 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt @@ -19,14 +19,15 @@ package im.vector.app.features.settings.devices.v2.overview import androidx.lifecycle.asFlow import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.settings.devices.v2.DeviceFullInfo -import im.vector.app.features.settings.devices.v2.GetCurrentSessionCrossSigningInfoUseCase -import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase +import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase +import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase +import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow -import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject class GetDeviceFullInfoUseCase @Inject constructor( @@ -34,9 +35,10 @@ class GetDeviceFullInfoUseCase @Inject constructor( private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, + private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, ) { - fun execute(deviceId: String): Flow> { + fun execute(deviceId: String): Flow { return activeSessionHolder.getSafeActiveSession()?.let { session -> combine( getCurrentSessionCrossSigningInfoUseCase.execute(), @@ -48,17 +50,21 @@ class GetDeviceFullInfoUseCase @Inject constructor( val fullInfo = if (info != null && cryptoInfo != null) { val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo) val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) + val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId + val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.getBestLastSeenUserAgent()) DeviceFullInfo( deviceInfo = info, cryptoDeviceInfo = cryptoInfo, roomEncryptionTrustLevel = roomEncryptionTrustLevel, - isInactive = isInactive + isInactive = isInactive, + isCurrentDevice = isCurrentDevice, + deviceExtendedInfo = deviceUserAgent, ) } else { null } fullInfo.toOptional() - } + }.unwrap() } ?: emptyFlow() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index c028c08ec4..42e79ac89f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * 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. @@ -18,4 +18,10 @@ package im.vector.app.features.settings.devices.v2.overview import im.vector.app.core.platform.VectorViewModelAction -sealed class SessionOverviewAction : VectorViewModelAction +sealed class SessionOverviewAction : VectorViewModelAction { + object VerifySession : SessionOverviewAction() + object SignoutOtherSession : SessionOverviewAction() + object SsoAuthDone : SessionOverviewAction() + data class PasswordAuthDone(val password: String) : SessionOverviewAction() + object ReAuthCancelled : SessionOverviewAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntryView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntryView.kt new file mode 100644 index 0000000000..5c4f0047ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntryView.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.overview + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import im.vector.app.R +import im.vector.app.core.extensions.setAttributeBackground +import im.vector.app.databinding.ViewSessionOverviewEntryBinding + +class SessionOverviewEntryView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewSessionOverviewEntryBinding.inflate( + LayoutInflater.from(context), + this + ) + + init { + initBackground() + context.obtainStyledAttributes( + attrs, + R.styleable.SessionOverviewEntryView, + 0, + 0 + ).use { + setTitle(it) + setDescription(it) + } + } + + private fun initBackground() { + binding.root.setAttributeBackground(android.R.attr.selectableItemBackground) + } + + private fun setTitle(typedArray: TypedArray) { + val title = typedArray.getString(R.styleable.SessionOverviewEntryView_sessionOverviewEntryTitle) + binding.sessionsOverviewEntryTitle.text = title + } + + private fun setDescription(typedArray: TypedArray) { + val description = typedArray.getString(R.styleable.SessionOverviewEntryView_sessionOverviewEntryDescription) + binding.sessionsOverviewEntryDescription.text = description + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index c5cd80bd3c..8c3b907070 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -16,26 +16,36 @@ package im.vector.app.features.settings.devices.v2.overview +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentSessionOverviewBinding -import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.workers.signout.SignOutUiWorker +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject /** @@ -43,7 +53,10 @@ import javax.inject.Inject */ @AndroidEntryPoint class SessionOverviewFragment : - VectorBaseFragment() { + VectorBaseFragment(), + VectorMenuProvider { + + @Inject lateinit var viewNavigator: SessionOverviewViewNavigator @Inject lateinit var dateFormatter: VectorDateFormatter @@ -59,7 +72,29 @@ class SessionOverviewFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + observeViewEvents() initSessionInfoView() + initVerifyButton() + initSignoutButton() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is SessionOverviewViewEvent.ShowVerifyCurrentSession -> { + navigator.requestSelfSessionVerification(requireActivity()) + } + is SessionOverviewViewEvent.ShowVerifyOtherSession -> { + navigator.requestSessionVerification(requireActivity(), it.deviceId) + } + is SessionOverviewViewEvent.PromptResetSecrets -> { + navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) + } + is SessionOverviewViewEvent.RequestReAuth -> askForReAuthentication(it) + SessionOverviewViewEvent.SignoutSuccess -> viewNavigator.goBack(requireActivity()) + is SessionOverviewViewEvent.SignoutError -> showFailure(it.error) + } + } } private fun initSessionInfoView() { @@ -68,6 +103,47 @@ class SessionOverviewFragment : } } + private fun initVerifyButton() { + views.sessionOverviewInfo.viewVerifyButton.debouncedClicks { + viewModel.handle(SessionOverviewAction.VerifySession) + } + } + + private fun initSignoutButton() { + views.sessionOverviewSignout.debouncedClicks { + confirmSignoutSession() + } + } + + private fun confirmSignoutSession() = withState(viewModel) { state -> + if (state.deviceInfo.invoke()?.isCurrentDevice.orFalse()) { + confirmSignoutCurrentSession() + } else { + confirmSignoutOtherSession() + } + } + + private fun confirmSignoutCurrentSession() { + activity?.let { SignOutUiWorker(it).perform() } + } + + private fun confirmSignoutOtherSession() { + activity?.let { + MaterialAlertDialogBuilder(it) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + signoutSession() + } + .setNegativeButton(R.string.action_cancel, null) + .show() + } + } + + private fun signoutSession() { + viewModel.handle(SessionOverviewAction.SignoutOtherSession) + } + override fun onDestroyView() { cleanUpSessionInfoView() super.onDestroyView() @@ -77,35 +153,123 @@ class SessionOverviewFragment : views.sessionOverviewInfo.onLearnMoreClickListener = null } + override fun getMenuRes() = R.menu.menu_session_overview + + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.sessionOverviewRename -> { + goToRenameSession() + true + } + else -> false + } + } + + private fun goToRenameSession() = withState(viewModel) { state -> + viewNavigator.goToRenameSession(requireContext(), state.deviceId) + } + override fun invalidate() = withState(viewModel) { state -> - updateToolbar(state.isCurrentSession) - if (state.deviceInfo is Success) { - renderSessionInfo(state.isCurrentSession, state.deviceInfo.invoke()) + updateToolbar(state) + updateEntryDetails(state.deviceId) + updateSessionInfo(state) + updateLoading(state.isLoading) + } + + private fun updateToolbar(viewState: SessionOverviewViewState) { + if (viewState.deviceInfo is Success) { + val titleResId = + if (viewState.deviceInfo.invoke().isCurrentDevice) R.string.device_manager_current_session_title else R.string.device_manager_session_title + (activity as? AppCompatActivity) + ?.supportActionBar + ?.setTitle(titleResId) + } + } + + private fun updateEntryDetails(deviceId: String) { + views.sessionOverviewEntryDetails.setOnClickListener { + viewNavigator.goToSessionDetails(requireContext(), deviceId) + } + } + + private fun updateSessionInfo(viewState: SessionOverviewViewState) { + if (viewState.deviceInfo is Success) { + views.sessionOverviewInfo.isVisible = true + val deviceInfo = viewState.deviceInfo.invoke() + val isCurrentSession = deviceInfo.isCurrentDevice + val infoViewState = SessionInfoViewState( + isCurrentSession = isCurrentSession, + deviceFullInfo = deviceInfo, + isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, + isDetailsButtonVisible = false, + isLearnMoreLinkVisible = true, + isLastSeenDetailsVisible = true, + ) + views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider) + views.sessionOverviewInfo.onLearnMoreClickListener = { + showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) + } + } else { + views.sessionOverviewInfo.isVisible = false + } + } + + private fun updateLoading(isLoading: Boolean) { + if (isLoading) { + showLoading(null) } else { - hideSessionInfo() + dismissLoadingDialog() } } - private fun updateToolbar(isCurrentSession: Boolean) { - val titleResId = if (isCurrentSession) R.string.device_manager_current_session_title else R.string.device_manager_session_title - (activity as? AppCompatActivity) - ?.supportActionBar - ?.setTitle(titleResId) + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(SessionOverviewAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(SessionOverviewAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(SessionOverviewAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(SessionOverviewAction.ReAuthCancelled) + } } - private fun renderSessionInfo(isCurrentSession: Boolean, deviceFullInfo: DeviceFullInfo) { - views.sessionOverviewInfo.isVisible = true - val viewState = SessionInfoViewState( - isCurrentSession = isCurrentSession, - deviceFullInfo = deviceFullInfo, - isDetailsButtonVisible = false, - isLearnMoreLinkVisible = true, - isLastSeenDetailsVisible = true, - ) - views.sessionOverviewInfo.render(viewState, dateFormatter, drawableProvider, colorProvider) + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: SessionOverviewViewEvent.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } } - private fun hideSessionInfo() { - views.sessionOverviewInfo.isGone = true + private fun showLearnMoreInfoVerificationStatus(isVerified: Boolean) { + val titleResId = if (isVerified) { + R.string.device_manager_verification_status_verified + } else { + R.string.device_manager_verification_status_unverified + } + val descriptionResId = if (isVerified) { + R.string.device_manager_learn_more_sessions_verified + } else { + R.string.device_manager_learn_more_sessions_unverified + } + val args = SessionLearnMoreBottomSheet.Args( + title = getString(titleResId), + description = getString(descriptionResId), + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt new file mode 100644 index 0000000000..4ff31d1a81 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.overview + +import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse + +sealed class SessionOverviewViewEvent : VectorViewEvents { + object ShowVerifyCurrentSession : SessionOverviewViewEvent() + data class ShowVerifyOtherSession(val deviceId: String) : SessionOverviewViewEvent() + object PromptResetSecrets : SessionOverviewViewEvent() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : SessionOverviewViewEvent() + + object SignoutSuccess : SessionOverviewViewEvent() + data class SignoutError(val error: Throwable) : SessionOverviewViewEvent() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 1a1d3640a2..bd5c7725eb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -21,20 +21,47 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyViewEvents -import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler +import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.session.Session +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, - session: Session, + private val stringProvider: StringProvider, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, -) : VectorViewModel(initialState) { + private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + private val signoutSessionUseCase: SignoutSessionUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, + private val activeSessionHolder: ActiveSessionHolder, + refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorSessionsListViewModel( + initialState, activeSessionHolder, refreshDevicesUseCase +) { companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() @@ -44,22 +71,131 @@ class SessionOverviewViewModel @AssistedInject constructor( } init { - val currentDeviceId = session.sessionParams.deviceId.orEmpty() - setState { - copy(isCurrentSession = deviceId.isNotEmpty() && deviceId == currentDeviceId) - } - observeSessionInfo(initialState.deviceId) + observeCurrentSessionInfo() } private fun observeSessionInfo(deviceId: String) { getDeviceFullInfoUseCase.execute(deviceId) - .mapNotNull { it.getOrNull() } .onEach { setState { copy(deviceInfo = Success(it)) } } .launchIn(viewModelScope) } + private fun observeCurrentSessionInfo() { + activeSessionHolder.getSafeActiveSession() + ?.sessionParams + ?.deviceId + ?.let { deviceId -> + getDeviceFullInfoUseCase.execute(deviceId) + .map { it.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted } + .distinctUntilChanged() + .onEach { setState { copy(isCurrentSessionTrusted = it) } } + .launchIn(viewModelScope) + } + } + override fun handle(action: SessionOverviewAction) { - TODO("Implement when adding the first action") + when (action) { + is SessionOverviewAction.VerifySession -> handleVerifySessionAction() + SessionOverviewAction.SignoutOtherSession -> handleSignoutOtherSession() + SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone() + is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action) + SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled() + } + } + + private fun handleVerifySessionAction() = withState { viewState -> + if (viewState.deviceInfo.invoke()?.isCurrentDevice.orFalse()) { + handleVerifyCurrentSession() + } else { + handleVerifyOtherSession(viewState.deviceId) + } + } + + private fun handleVerifyCurrentSession() { + viewModelScope.launch { + val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + if (currentSessionCanBeVerified) { + _viewEvents.post(SessionOverviewViewEvent.ShowVerifyCurrentSession) + } else { + _viewEvents.post(SessionOverviewViewEvent.PromptResetSecrets) + } + } + } + + private fun handleVerifyOtherSession(deviceId: String) { + _viewEvents.post(SessionOverviewViewEvent.ShowVerifyOtherSession(deviceId)) + } + + private fun handleSignoutOtherSession() = withState { state -> + // signout process for current session is not handled here + if (!state.deviceInfo.invoke()?.isCurrentDevice.orFalse()) { + handleSignoutOtherSession(state.deviceId) + } + } + + private fun handleSignoutOtherSession(deviceId: String) { + viewModelScope.launch { + setLoading(true) + val signoutResult = signout(deviceId) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit + } + } + }) + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(SessionOverviewViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(SessionOverviewViewEvent.SignoutSuccess) + } + + private fun onSignoutFailure(failure: Throwable) { + Timber.e("signout failure", failure) + val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + stringProvider.getString(R.string.authentication_error) + } else { + stringProvider.getString(R.string.matrix_error) + } + _viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: SessionOverviewAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt new file mode 100644 index 0000000000..0cb95bae7d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.overview + +import android.content.Context +import androidx.fragment.app.FragmentActivity +import im.vector.app.features.settings.devices.v2.details.SessionDetailsActivity +import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity +import javax.inject.Inject + +class SessionOverviewViewNavigator @Inject constructor() { + + fun goToSessionDetails(context: Context, deviceId: String) { + context.startActivity(SessionDetailsActivity.newIntent(context, deviceId)) + } + + fun goToRenameSession(context: Context, deviceId: String) { + context.startActivity(RenameSessionActivity.newIntent(context, deviceId)) + } + + fun goBack(fragmentActivity: FragmentActivity) { + fragmentActivity.finish() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index a447336c23..07423888b5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -23,8 +23,9 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo data class SessionOverviewViewState( val deviceId: String, - val isCurrentSession: Boolean = false, + val isCurrentSessionTrusted: Boolean = false, val deviceInfo: Async = Uninitialized, + val isLoading: Boolean = false, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt new file mode 100644 index 0000000000..c60bc7df14 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class RenameSessionAction : VectorViewModelAction { + object InitWithLastEditedName : RenameSessionAction() + object SaveModifications : RenameSessionAction() + data class EditLocally(val editedName: String) : RenameSessionAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt new file mode 100644 index 0000000000..eb0d994ce3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.WindowManager +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding + +/** + * Display the screen to rename a Session. + */ +@AndroidEntryPoint +class RenameSessionActivity : VectorBaseActivity() { + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + addFragment( + container = views.simpleFragmentContainer, + fragmentClass = RenameSessionFragment::class.java, + params = intent.getParcelableExtra(Mavericks.KEY_ARG) + ) + } + } + + companion object { + fun newIntent(context: Context, deviceId: String): Intent { + return Intent(context, RenameSessionActivity::class.java).apply { + putExtra(Mavericks.KEY_ARG, RenameSessionArgs(deviceId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt new file mode 100644 index 0000000000..d43d472946 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RenameSessionArgs( + val deviceId: String +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt new file mode 100644 index 0000000000..2f671492e3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.showKeyboard +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSessionRenameBinding +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import javax.inject.Inject + +/** + * Display the screen to rename a Session. + */ +@AndroidEntryPoint +class RenameSessionFragment : + VectorBaseFragment() { + + private val viewModel: RenameSessionViewModel by fragmentViewModel() + + @Inject lateinit var viewNavigator: RenameSessionViewNavigator + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionRenameBinding { + return FragmentSessionRenameBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeViewEvents() + initToolbar() + initEditText() + initSaveButton() + initWithLastEditedName() + initInfoView() + } + + private fun initToolbar() { + setupToolbar(views.renameSessionToolbar) + .allowBack(useCross = true) + } + + private fun initEditText() { + views.renameSessionEditText.showKeyboard(andRequestFocus = true) + views.renameSessionEditText.doOnTextChanged { text, _, _, _ -> + viewModel.handle(RenameSessionAction.EditLocally(text.toString())) + } + } + + private fun initSaveButton() { + views.renameSessionSave.debouncedClicks { + viewModel.handle(RenameSessionAction.SaveModifications) + } + } + + private fun initWithLastEditedName() { + viewModel.handle(RenameSessionAction.InitWithLastEditedName) + } + + private fun initInfoView() { + views.renameSessionInfo.onLearnMoreClickListener = { + showLearnMoreInfo() + } + } + + private fun showLearnMoreInfo() { + val args = SessionLearnMoreBottomSheet.Args( + title = getString(R.string.device_manager_learn_more_session_rename_title), + description = getString(R.string.device_manager_learn_more_session_rename), + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is RenameSessionViewEvent.Initialized -> { + views.renameSessionEditText.setText(it.deviceName) + views.renameSessionEditText.setSelection(views.renameSessionEditText.length()) + } + is RenameSessionViewEvent.SessionRenamed -> { + viewNavigator.goBack(requireActivity()) + } + is RenameSessionViewEvent.Failure -> { + showFailure(it.throwable) + } + } + } + } + + override fun invalidate() = withState(viewModel) { state -> + views.renameSessionSave.isEnabled = state.editedDeviceName.isNotEmpty() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt new file mode 100644 index 0000000000..2e44bb33d6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.andThen +import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import org.matrix.android.sdk.api.util.awaitCallback +import javax.inject.Inject + +class RenameSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val refreshDevicesUseCase: RefreshDevicesUseCase, +) { + + suspend fun execute(deviceId: String, newName: String): Result { + return renameDevice(deviceId, newName) + .andThen { refreshDevices() } + } + + private suspend fun renameDevice(deviceId: String, newName: String) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .setDeviceName(deviceId, newName, matrixCallback) + } + } + + private fun refreshDevices() = runCatching { refreshDevicesUseCase.execute() } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt new file mode 100644 index 0000000000..fd40412547 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import im.vector.app.core.platform.VectorViewEvents + +sealed class RenameSessionViewEvent : VectorViewEvents { + data class Initialized(val deviceName: String) : RenameSessionViewEvent() + object SessionRenamed : RenameSessionViewEvent() + data class Failure(val throwable: Throwable) : RenameSessionViewEvent() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt new file mode 100644 index 0000000000..22170fc702 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import androidx.annotation.VisibleForTesting +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch + +class RenameSessionViewModel @AssistedInject constructor( + @Assisted val initialState: RenameSessionViewState, + private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, + private val renameSessionUseCase: RenameSessionUseCase, +) : VectorViewModel(initialState) { + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RenameSessionViewState): RenameSessionViewModel + } + + @VisibleForTesting + var hasRetrievedOriginalDeviceName = false + + override fun handle(action: RenameSessionAction) { + when (action) { + is RenameSessionAction.InitWithLastEditedName -> handleInitWithLastEditedName() + is RenameSessionAction.EditLocally -> handleEditLocally(action.editedName) + is RenameSessionAction.SaveModifications -> handleSaveModifications() + } + } + + private fun handleInitWithLastEditedName() = withState { state -> + if (hasRetrievedOriginalDeviceName) { + postInitEvent() + } else { + hasRetrievedOriginalDeviceName = true + viewModelScope.launch { + setStateWithOriginalDeviceName(state.deviceId) + postInitEvent() + } + } + } + + private suspend fun setStateWithOriginalDeviceName(deviceId: String) { + getDeviceFullInfoUseCase.execute(deviceId) + .firstOrNull() + ?.let { deviceFullInfo -> + setState { copy(editedDeviceName = deviceFullInfo.deviceInfo.displayName.orEmpty()) } + } + } + + private fun postInitEvent() = withState { state -> + _viewEvents.post(RenameSessionViewEvent.Initialized(state.editedDeviceName)) + } + + private fun handleEditLocally(editedName: String) { + setState { copy(editedDeviceName = editedName) } + } + + private fun handleSaveModifications() = withState { viewState -> + viewModelScope.launch { + val result = renameSessionUseCase.execute( + deviceId = viewState.deviceId, + newName = viewState.editedDeviceName, + ) + val viewEvent = if (result.isSuccess) { + RenameSessionViewEvent.SessionRenamed + } else { + RenameSessionViewEvent.Failure(result.exceptionOrNull() ?: Exception()) + } + _viewEvents.post(viewEvent) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt new file mode 100644 index 0000000000..e00d7b25ff --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import androidx.fragment.app.FragmentActivity +import javax.inject.Inject + +class RenameSessionViewNavigator @Inject constructor() { + + fun goBack(activity: FragmentActivity) { + activity.finish() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt new file mode 100644 index 0000000000..70e11327ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.rename + +import com.airbnb.mvrx.MavericksState + +data class RenameSessionViewState( + val deviceId: String, + val editedDeviceName: String = "", +) : MavericksState { + constructor(args: RenameSessionArgs) : this( + deviceId = args.deviceId + ) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt new file mode 100644 index 0000000000..4316995272 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.login.ReAuthHelper +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +class InterceptSignoutFlowResponseUseCase @Inject constructor( + private val reAuthHelper: ReAuthHelper, + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute( + flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation + ): SignoutSessionResult { + return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = activeSessionHolder.getActiveSession().myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + + SignoutSessionResult.Completed + } else { + SignoutSessionResult.ReAuthNeeded( + pendingAuth = DefaultBaseAuth(session = flowResponse.session), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errCode + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt new file mode 100644 index 0000000000..fa1fb31b66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import kotlin.coroutines.Continuation + +sealed class SignoutSessionResult { + data class ReAuthNeeded( + val pendingAuth: UIABaseAuth, + val uiaContinuation: Continuation, + val flowResponse: RegistrationFlowResponse, + val errCode: String? + ) : SignoutSessionResult() + + object Completed : SignoutSessionResult() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt new file mode 100644 index 0000000000..60ca8e91c6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.util.awaitCallback +import javax.inject.Inject + +class SignoutSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { + return deleteDevice(deviceId, userInteractiveAuthInterceptor) + } + + private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt new file mode 100644 index 0000000000..3fdef1a98f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.verification + +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.firstOrNull +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.flow.flow +import timber.log.Timber +import javax.inject.Inject + +class CheckIfCurrentSessionCanBeVerifiedUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(): Boolean { + val session = activeSessionHolder.getSafeActiveSession() + val cryptoSessionsCount = session?.flow() + ?.liveUserCryptoDevices(session.myUserId) + ?.firstOrNull() + ?.size + ?: 0 + val hasOtherSessions = cryptoSessionsCount > 1 + + val isRecoverySetup = session + ?.sharedSecretStorageService() + ?.isRecoverySetup() + .orFalse() + + Timber.d("hasOtherSessions=$hasOtherSessions (otherSessionsCount=$cryptoSessionsCount), isRecoverySetup=$isRecoverySetup") + return hasOtherSessions || isRecoverySetup + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/CurrentSessionCrossSigningInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CurrentSessionCrossSigningInfo.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/CurrentSessionCrossSigningInfo.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CurrentSessionCrossSigningInfo.kt index cccdb23d52..1dfab06b1d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/CurrentSessionCrossSigningInfo.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CurrentSessionCrossSigningInfo.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.features.settings.devices.v2.verification /** * Used to hold some info about the cross signing of the current Session. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetCurrentSessionCrossSigningInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetCurrentSessionCrossSigningInfoUseCase.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/GetCurrentSessionCrossSigningInfoUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetCurrentSessionCrossSigningInfoUseCase.kt index cc848342de..182e493213 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetCurrentSessionCrossSigningInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetCurrentSessionCrossSigningInfoUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.features.settings.devices.v2.verification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt index 7e56d35570..2243471a23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.features.settings.devices.v2.verification import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForDeviceUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt index 6f0dcbface..ba9a380ade 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForDeviceUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.features.settings.devices.v2.verification import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForOtherDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForOtherDeviceUseCase.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForOtherDeviceUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForOtherDeviceUseCase.kt index 7541b9b1d5..e674ccd8c5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetEncryptionTrustLevelForOtherDeviceUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForOtherDeviceUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2 +package im.vector.app.features.settings.devices.v2.verification import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt index 9853b28aae..0cbfef7495 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerController.kt @@ -37,13 +37,15 @@ import javax.inject.Inject class LocalePickerController @Inject constructor( private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter, + private val vectorLocale: VectorLocale, ) : TypedEpoxyController() { var listener: Listener? = null override fun buildModels(data: LocalePickerViewState?) { val list = data?.locales ?: return + val currentLocale = data.currentLocale ?: return val host = this profileSectionItem { @@ -51,10 +53,10 @@ class LocalePickerController @Inject constructor( title(host.stringProvider.getString(R.string.choose_locale_current_locale_title)) } localeItem { - id(data.currentLocale.toString()) - title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale)) + id(currentLocale.toString()) + title(host.vectorLocale.localeToLocalisedString(currentLocale).safeCapitalize(currentLocale)) if (host.vectorPreferences.developerMode()) { - subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) + subtitle(host.vectorLocale.localeToLocalisedStringInfo(currentLocale)) } clickListener { host.listener?.onUseCurrentClicked() } } @@ -78,13 +80,13 @@ class LocalePickerController @Inject constructor( } } else { list() - .filter { it.toString() != data.currentLocale.toString() } + .filter { it.toString() != currentLocale.toString() } .forEach { locale -> localeItem { id(locale.toString()) - title(VectorLocale.localeToLocalisedString(locale).safeCapitalize(locale)) + title(host.vectorLocale.localeToLocalisedString(locale).safeCapitalize(locale)) if (host.vectorPreferences.developerMode()) { - subtitle(VectorLocale.localeToLocalisedStringInfo(locale)) + subtitle(host.vectorLocale.localeToLocalisedStringInfo(locale)) } clickListener { host.listener?.onLocaleClicked(locale) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt index 0bbbc323e0..c38f9b5b87 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt @@ -30,7 +30,8 @@ import kotlinx.coroutines.launch class LocalePickerViewModel @AssistedInject constructor( @Assisted initialState: LocalePickerViewState, - private val vectorConfiguration: VectorConfiguration + private val vectorConfiguration: VectorConfiguration, + private val vectorLocale: VectorLocale, ) : VectorViewModel(initialState) { @AssistedFactory @@ -39,8 +40,13 @@ class LocalePickerViewModel @AssistedInject constructor( } init { + setState { + copy( + currentLocale = vectorLocale.applicationLocale + ) + } viewModelScope.launch { - val result = VectorLocale.getSupportedLocales() + val result = vectorLocale.getSupportedLocales() setState { copy( @@ -59,7 +65,7 @@ class LocalePickerViewModel @AssistedInject constructor( } private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { - VectorLocale.saveApplicationLocale(action.locale) + vectorLocale.saveApplicationLocale(action.locale) vectorConfiguration.applyToApplicationContext() _viewEvents.post(LocalePickerViewEvents.RestartActivity) } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt index 8cb5978393..f981e7a444 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt @@ -19,10 +19,9 @@ package im.vector.app.features.settings.locale import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized -import im.vector.app.features.settings.VectorLocale import java.util.Locale data class LocalePickerViewState( - val currentLocale: Locale = VectorLocale.applicationLocale, + val currentLocale: Locale? = null, val locales: Async> = Uninitialized ) : MavericksState 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 d80553b0ed..249df0007f 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 @@ -28,33 +28,26 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ReadOnceTrue -import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.auth.PendingAuthHandler import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.Matrix 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.session.Session import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.flow.flow -import timber.log.Timber import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException class ThreePidsSettingsViewModel @AssistedInject constructor( @Assisted initialState: ThreePidsSettingsViewState, private val session: Session, private val stringProvider: StringProvider, - private val matrix: Matrix, + private val pendingAuthHandler: PendingAuthHandler, ) : VectorViewModel(initialState) { // UIA session private var pendingThreePid: ThreePid? = null -// private var pendingSession: String? = null private suspend fun loadingSuspendable(block: suspend () -> Unit) { runCatching { block() } @@ -126,42 +119,17 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) - ThreePidsSettingsAction.SsoAuthDone -> { - Timber.d("## UIA - FallBack success") - if (pendingAuth != null) { - uiaContinuation?.resume(pendingAuth!!) - } else { - uiaContinuation?.resumeWithException(IllegalArgumentException()) - } - } - is ThreePidsSettingsAction.PasswordAuthDone -> { - val decryptedPass = matrix.secureStorageService() - .loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) - uiaContinuation?.resume( - UserPasswordAuth( - session = pendingAuth?.session, - password = decryptedPass, - user = session.myUserId - ) - ) - } - ThreePidsSettingsAction.ReAuthCancelled -> { - Timber.d("## UIA - Reauth cancelled") - uiaContinuation?.resumeWithException(Exception()) - uiaContinuation = null - pendingAuth = null - } + ThreePidsSettingsAction.SsoAuthDone -> pendingAuthHandler.ssoAuthDone() + is ThreePidsSettingsAction.PasswordAuthDone -> pendingAuthHandler.passwordAuthDone(action.password) + ThreePidsSettingsAction.ReAuthCancelled -> pendingAuthHandler.reAuthCancelled() } } - var uiaContinuation: Continuation? = null - var pendingAuth: UIABaseAuth? = null - private val uiaInterceptor = object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { _viewEvents.post(ThreePidsSettingsViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = flowResponse.session) + pendingAuthHandler.uiaContinuation = promise } } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt index 6eede93143..0c556192ac 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareController.kt @@ -60,6 +60,7 @@ class IncomingShareController @Inject constructor( roomSummary, data.selectedRoomIds, RoomListDisplayMode.FILTERED, + singleLineLastEvent = false, callback?.let { it::onRoomClicked }, callback?.let { it::onRoomLongClicked } ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt index 5061eb4036..199169484c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt @@ -72,7 +72,7 @@ class NewSpaceSummaryController @Inject constructor( text(host.stringProvider.getString(R.string.all_chats)) selected(selected) countState(UnreadCounterBadgeView.State.Count(homeCount.totalCount, homeCount.isHighlight)) - listener { host.callback?.onSpaceSelected(null) } + listener { host.callback?.onSpaceSelected(null, isSubSpace = false) } } } @@ -99,7 +99,7 @@ class NewSpaceSummaryController @Inject constructor( hasChildren(hasChildren) matrixItem(spaceSummary.toMatrixItem()) onLongClickListener { host.callback?.onSpaceSettings(spaceSummary) } - onSpaceSelectedListener { host.callback?.onSpaceSelected(spaceSummary) } + onSpaceSelectedListener { host.callback?.onSpaceSelected(spaceSummary, isSubSpace = false) } onToggleExpandListener { host.callback?.onToggleExpand(spaceSummary) } selected(isSelected) } @@ -140,7 +140,7 @@ class NewSpaceSummaryController @Inject constructor( indent(depth) matrixItem(childSummary.toMatrixItem()) onLongClickListener { host.callback?.onSpaceSettings(childSummary) } - onSubSpaceSelectedListener { host.callback?.onSpaceSelected(childSummary) } + onSubSpaceSelectedListener { host.callback?.onSpaceSelected(childSummary, isSubSpace = true) } onToggleExpandListener { host.callback?.onToggleExpand(childSummary) } selected(isSelected) } @@ -184,8 +184,10 @@ class NewSpaceSummaryController @Inject constructor( } } + /** + * This is a full duplicate of [SpaceSummaryController.Callback]. We need to merge them ASAP*/ interface Callback { - fun onSpaceSelected(spaceSummary: RoomSummary?) + fun onSpaceSelected(spaceSummary: RoomSummary?, isSubSpace: Boolean) fun onSpaceInviteSelected(spaceSummary: RoomSummary) fun onSpaceSettings(spaceSummary: RoomSummary) fun onToggleExpand(spaceSummary: RoomSummary) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt index fd2e68e172..1ef755e684 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt @@ -20,7 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.room.model.RoomSummary sealed class SpaceListAction : VectorViewModelAction { - data class SelectSpace(val spaceSummary: RoomSummary?) : SpaceListAction() + data class SelectSpace(val spaceSummary: RoomSummary?, val isSubSpace: Boolean) : SpaceListAction() data class OpenSpaceInvite(val spaceSummary: RoomSummary) : SpaceListAction() data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction() data class ToggleExpand(val spaceSummary: RoomSummary) : SpaceListAction() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt index 910f8c5379..9991384643 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListBottomSheet.kt @@ -16,25 +16,38 @@ package im.vector.app.features.spaces +import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.app.R import im.vector.app.core.extensions.replaceChildFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.FragmentSpacesBottomSheetBinding +import im.vector.app.features.analytics.plan.MobileScreen -class SpaceListBottomSheet : BottomSheetDialogFragment() { +class SpaceListBottomSheet : VectorBaseBottomSheetDialogFragment() { - private lateinit var binding: FragmentSpacesBottomSheetBinding + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSpacesBottomSheetBinding { + return FragmentSpacesBottomSheetBinding.inflate(inflater, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = MobileScreen.ScreenName.SpaceBottomSheet + } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentSpacesBottomSheetBinding.inflate(inflater, container, false) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (savedInstanceState == null) { replaceChildFragment(R.id.space_list, SpaceListFragment::class.java) } - return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setPeekHeightAsScreenPercentage(0.75f) + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index 27a118e4dc..550db1d0d1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -176,8 +176,8 @@ class SpaceListFragment : } } - override fun onSpaceSelected(spaceSummary: RoomSummary?) { - viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) + override fun onSpaceSelected(spaceSummary: RoomSummary?, isSubSpace: Boolean) { + viewModel.handle(SpaceListAction.SelectSpace(spaceSummary, isSubSpace = isSubSpace)) roomListSharedActionViewModel.post(RoomListSharedAction.CloseBottomSheet) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index 9cd456b5d7..99f6a254b8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.session.Session 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.getUserOrDefault import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.Membership @@ -214,7 +215,18 @@ class SpaceListViewModel @AssistedInject constructor( private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> if (state.selectedSpace?.roomId != action.spaceSummary?.roomId) { - analyticsTracker.capture(Interaction(null, null, Interaction.Name.SpacePanelSwitchSpace)) + val interactionName = if (action.isSubSpace) { + Interaction.Name.SpacePanelSwitchSubSpace + } else { + Interaction.Name.SpacePanelSwitchSpace + } + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = interactionName + ) + ) setState { copy(selectedSpace = action.spaceSummary) } spaceStateHandler.setCurrentSpace(action.spaceSummary?.roomId) _viewEvents.post(SpaceListViewEvents.CloseDrawer) @@ -272,7 +284,7 @@ class SpaceListViewModel @AssistedInject constructor( ?.safeOrder() } val inviterIds = spaces.mapNotNull { it.inviterId } - val inviters = inviterIds.mapNotNull { session.userService().getUser(it) } + val inviters = inviterIds.map { session.getUserOrDefault(it) } copy( asyncSpaces = asyncSpaces, spaces = spaces, diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index ff8f5c38f7..acc1df5405 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -88,7 +88,7 @@ class SpaceSummaryController @Inject constructor( id("space_home") selected(selectedSpace == null) countState(UnreadCounterBadgeView.State.Count(homeCount.totalCount, homeCount.isHighlight)) - listener { host.callback?.onSpaceSelected(null) } + listener { host.callback?.onSpaceSelected(null, isSubSpace = false) } } rootSpaces @@ -114,7 +114,7 @@ class SpaceSummaryController @Inject constructor( selected(isSelected) canDrag(true) onMore { host.callback?.onSpaceSettings(roomSummary) } - listener { host.callback?.onSpaceSelected(roomSummary) } + listener { host.callback?.onSpaceSelected(roomSummary, isSubSpace = false) } toggleExpand { host.callback?.onToggleExpand(roomSummary) } countState( UnreadCounterBadgeView.State.Count( @@ -165,7 +165,7 @@ class SpaceSummaryController @Inject constructor( expanded(expanded) onMore { host.callback?.onSpaceSettings(childSummary) } matrixItem(childSummary.toMatrixItem()) - listener { host.callback?.onSpaceSelected(childSummary) } + listener { host.callback?.onSpaceSelected(childSummary, isSubSpace = true) } toggleExpand { host.callback?.onToggleExpand(childSummary) } indent(currentDepth) countState( @@ -184,7 +184,7 @@ class SpaceSummaryController @Inject constructor( } interface Callback { - fun onSpaceSelected(spaceSummary: RoomSummary?) + fun onSpaceSelected(spaceSummary: RoomSummary?, isSubSpace: Boolean) fun onSpaceInviteSelected(spaceSummary: RoomSummary) fun onSpaceSettings(spaceSummary: RoomSummary) fun onToggleExpand(spaceSummary: RoomSummary) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt index 4c44bfc7a8..6c31b9e856 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt @@ -25,6 +25,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.epoxy.onClick import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceCreateChooseTypeBinding +import im.vector.app.features.analytics.plan.MobileScreen @AndroidEntryPoint class ChooseSpaceTypeFragment : @@ -35,6 +36,11 @@ class ChooseSpaceTypeFragment : override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceCreateChooseTypeBinding.inflate(layoutInflater, container, false) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = MobileScreen.ScreenName.CreateSpace + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index b680f77df2..1cfac4a5fe 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -32,6 +32,8 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.isEmail import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Interaction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns @@ -46,7 +48,8 @@ class CreateSpaceViewModel @AssistedInject constructor( private val session: Session, private val stringProvider: StringProvider, private val createSpaceViewModelTask: CreateSpaceViewModelTask, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter, + private val analyticsTracker: AnalyticsTracker, ) : VectorViewModel(initialState) { private val identityService = session.identityService() @@ -350,6 +353,13 @@ class CreateSpaceViewModel @AssistedInject constructor( } viewModelScope.launch(Dispatchers.IO) { try { + analyticsTracker.capture( + Interaction( + index = null, + interactionType = null, + name = Interaction.Name.MobileSpaceCreationValidated + ) + ) val alias = if (state.spaceType == SpaceType.Public) { state.aliasLocalPart } else null diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt index d7db9b123b..63d63329d7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt @@ -189,7 +189,7 @@ class SpaceManageRoomsViewModel @AssistedInject constructor( val apiResult = session.spaceService().querySpaceChildren( spaceId = initialState.spaceId, from = nextToken, - knownStateList = knownResults.childrenState.orEmpty(), + knownStateList = knownResults.childrenState, limit = paginationLimit ) val newKnown = apiResult.children.mapNotNull { session.getRoomSummary(it.childRoomId) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 5e6efcc816..3b74b4b38b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -77,7 +77,7 @@ class SpacePeopleListController @Inject constructor( id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) .apply { val pl = host.toPowerLevelLabel(memberEntry.first) if (memberEntry.first == RoomMemberListCategories.INVITE) { diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index 96af7906e2..b5c7b162d8 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -27,8 +27,8 @@ import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.graphics.drawable.DrawableCompat +import androidx.preference.PreferenceManager import im.vector.app.R -import im.vector.app.core.di.DefaultSharedPreferences import timber.log.Timber import java.util.concurrent.atomic.AtomicReference @@ -84,7 +84,7 @@ object ThemeUtils { fun getApplicationTheme(context: Context): String { val currentTheme = this.currentTheme.get() return if (currentTheme == null) { - val prefs = DefaultSharedPreferences.getInstance(context) + val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, DEFAULT_THEME) ?: DEFAULT_THEME if (themeFromPref == "status") { // Migrate to the default theme 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 c8eeb45635..d5395cc849 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 @@ -42,6 +42,7 @@ import kotlin.coroutines.CoroutineContext class VoiceRecorderL( context: Context, coroutineContext: CoroutineContext, + private val codec: OggOpusEncoder, ) : VoiceRecorder { companion object { @@ -58,7 +59,6 @@ class VoiceRecorderL( private var audioRecorder: AudioRecord? = null private var noiseSuppressor: NoiseSuppressor? = null private var automaticGainControl: AutomaticGainControl? = null - private val codec = OggOpusEncoder() // Size of the audio buffer for Short values private var bufferSizeInShorts = 0 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 1bf289fb4c..38771be44e 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 @@ -20,25 +20,32 @@ import android.content.Context import android.media.MediaCodecList import android.media.MediaFormat import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.VisibleForTesting import im.vector.app.features.VectorFeatures +import io.element.android.opusencoder.OggOpusEncoder import kotlinx.coroutines.Dispatchers +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import javax.inject.Inject class VoiceRecorderProvider @Inject constructor( private val context: Context, private val vectorFeatures: VectorFeatures, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, ) { fun provideVoiceRecorder(): VoiceRecorder { - return if (useFallbackRecorder()) { - VoiceRecorderL(context, Dispatchers.IO) - } else { + return if (useNativeRecorder()) { VoiceRecorderQ(context) + } else { + VoiceRecorderL(context, Dispatchers.IO, OggOpusEncoder.create()) } } - private fun useFallbackRecorder(): Boolean { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !hasOpusEncoder() || vectorFeatures.forceUsageOfOpusEncoder() + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + private fun useNativeRecorder(): Boolean { + return buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.Q && + hasOpusEncoder() && + !vectorFeatures.forceUsageOfOpusEncoder() } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index ac9930866f..254a7f97f5 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -16,7 +16,6 @@ package im.vector.app.features.widgets.webview -import android.annotation.SuppressLint import android.app.Activity import android.view.ViewGroup import android.webkit.CookieManager @@ -29,7 +28,6 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.webview.VectorWebViewClient import im.vector.app.features.webview.WebEventListener -@SuppressLint("NewApi") fun WebView.setupForWidget(activity: Activity, checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase, eventListener: WebEventListener, diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusAction.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusAction.kt new file mode 100644 index 0000000000..2c59a80964 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.workers.signout + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface ServerBackupStatusAction : VectorViewModelAction { + data class OnRecoverDoneForVersion(val version: String) : ServerBackupStatusAction + object OnBannerDisplayed : ServerBackupStatusAction + object OnBannerClosed : ServerBackupStatusAction +} diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index 9105793d74..f3eb04b54e 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -16,6 +16,8 @@ package im.vector.app.features.workers.signout +import android.content.SharedPreferences +import androidx.core.content.edit import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState @@ -25,9 +27,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.BuildConfig +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -52,29 +54,55 @@ data class ServerBackupStatusViewState( * The state representing the view. * It can take one state at a time. */ -sealed class BannerState { +sealed interface BannerState { + // Not yet rendered + object Initial : BannerState - object Hidden : BannerState() + // View will be Gone + object Hidden : BannerState // Keys backup is not setup, numberOfKeys is the number of locally stored keys - data class Setup(val numberOfKeys: Int) : BannerState() + data class Setup(val numberOfKeys: Int, val doNotShowAgain: Boolean) : BannerState + + // Keys backup can be recovered, with version from the server + data class Recover(val version: String, val doNotShowForVersion: String) : BannerState + + // Keys backup can be updated + data class Update(val version: String, val doNotShowForVersion: String) : BannerState // Keys are backing up - object BackingUp : BannerState() + object BackingUp : BannerState } class ServerBackupStatusViewModel @AssistedInject constructor( @Assisted initialState: ServerBackupStatusViewState, - private val session: Session + private val session: Session, + @DefaultPreferences + private val sharedPreferences: SharedPreferences, ) : - VectorViewModel(initialState), KeysBackupStateListener { + VectorViewModel(initialState), KeysBackupStateListener { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + /** + * Preference key for setup. Value is a boolean. + */ + private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN" + + /** + * Preference key for recover. Value is a backup version (String). + */ + private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION" + + /** + * Preference key for update. Value is a backup version (String). + */ + private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION" + } // Keys exported manually val keysExportedToFile = MutableLiveData() @@ -113,7 +141,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor( pInfo.getOrNull()?.allKnown().orFalse()) ) { // So 4S is not setup and we have local secrets, - return@combine BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) + return@combine BannerState.Setup( + numberOfKeys = getNumberOfKeysToBackup(), + doNotShowAgain = sharedPreferences.getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) + ) } BannerState.Hidden } @@ -169,5 +200,47 @@ class ServerBackupStatusViewModel @AssistedInject constructor( } } - override fun handle(action: EmptyAction) {} + override fun handle(action: ServerBackupStatusAction) { + when (action) { + is ServerBackupStatusAction.OnRecoverDoneForVersion -> handleOnRecoverDoneForVersion(action) + ServerBackupStatusAction.OnBannerDisplayed -> handleOnBannerDisplayed() + ServerBackupStatusAction.OnBannerClosed -> handleOnBannerClosed() + } + } + + private fun handleOnRecoverDoneForVersion(action: ServerBackupStatusAction.OnRecoverDoneForVersion) { + sharedPreferences.edit { + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, action.version) + } + } + + private fun handleOnBannerDisplayed() { + sharedPreferences.edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") + } + } + + private fun handleOnBannerClosed() = withState { state -> + when (val bannerState = state.bannerState()) { + is BannerState.Setup -> { + sharedPreferences.edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) + } + } + is BannerState.Recover -> { + sharedPreferences.edit { + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, bannerState.version) + } + } + is BannerState.Update -> { + sharedPreferences.edit { + putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, bannerState.version) + } + } + else -> { + // Should not happen, close button is not displayed in other cases + } + } + } } diff --git a/vector/src/main/res/drawable/circle_with_transparent_border.xml b/vector/src/main/res/drawable/circle_with_transparent_border.xml new file mode 100644 index 0000000000..22b092a71e --- /dev/null +++ b/vector/src/main/res/drawable/circle_with_transparent_border.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_attachment_voice_broadcast.xml b/vector/src/main/res/drawable/ic_attachment_voice_broadcast.xml new file mode 100644 index 0000000000..3e93522b18 --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_voice_broadcast.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_shield_gray.xml b/vector/src/main/res/drawable/ic_shield_gray.xml new file mode 100644 index 0000000000..a4c52d74ba --- /dev/null +++ b/vector/src/main/res/drawable/ic_shield_gray.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/drawable/placeholder_shape_8.xml b/vector/src/main/res/drawable/placeholder_shape_8.xml index 503389788d..4e015d4a56 100644 --- a/vector/src/main/res/drawable/placeholder_shape_8.xml +++ b/vector/src/main/res/drawable/placeholder_shape_8.xml @@ -2,10 +2,9 @@ - - \ No newline at end of file + diff --git a/vector/src/main/res/drawable/poll_option_checked.xml b/vector/src/main/res/drawable/poll_option_checked.xml index 28ab94a421..2324792eac 100644 --- a/vector/src/main/res/drawable/poll_option_checked.xml +++ b/vector/src/main/res/drawable/poll_option_checked.xml @@ -10,5 +10,9 @@ - \ No newline at end of file + android:top="2dp" + android:bottom="2dp" + android:left="2dp" + android:right="2dp" + /> + diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml new file mode 100644 index 0000000000..a7987e70b5 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml b/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml new file mode 100644 index 0000000000..466ab5af49 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml @@ -0,0 +1,59 @@ + + + + + + + + + +