diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index e663a49fdb..b063c93530 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -29,7 +29,7 @@ body: - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` - [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance - - [ ] Run towncrier: `towncrier --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md) + - [ ] Run towncrier: `towncrier build --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md) - [ ] Check that the folder `changelog.d` is empty. It can happen that some remaining files stay here - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index d7f5ce8b57..8bc5efe860 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -21,7 +21,7 @@ jobs: - name: Run code quality check suite run: ./tools/check/check_code_quality.sh -# Knit for all the modules (https://github.com/Kotlin/kotlinx-knit) + # Knit for all the modules (https://github.com/Kotlin/kotlinx-knit) knit: name: Knit runs-on: ubuntu-latest @@ -31,7 +31,7 @@ jobs: run: | ./gradlew knit -# ktlint for all the modules + # ktlint for all the modules ktlint: name: Kotlin Linter runs-on: ubuntu-latest @@ -104,7 +104,7 @@ jobs: comment_id: ${{ steps.fc.outputs.comment-id }} }) -# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin + # Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin dependency-analysis: name: Dependency analysis runs-on: ubuntu-latest @@ -123,7 +123,7 @@ jobs: name: dependency-analysis path: build/reports/dependency-check-report.html -# Lint for main module + # Lint for main module android-lint: name: Android Linter runs-on: ubuntu-latest @@ -151,7 +151,7 @@ jobs: path: | vector/build/reports/*.* -# Lint for Gplay and Fdroid release APK + # Lint for Gplay and Fdroid release APK apk-lint: name: Lint APK (${{ matrix.target }}) runs-on: ubuntu-latest @@ -203,3 +203,26 @@ jobs: name: detekt-report path: | */build/reports/detekt/detekt.html + +# towncrier: +# name: Towncrier check +# runs-on: ubuntu-latest +# if: github.event_name == 'pull_request' && github.head_ref == 'develop' +# steps: +# - uses: actions/checkout@v3 +# - name: Set up Python 3.8 +# uses: actions/setup-python@v4 +# with: +# python-version: 3.8 +# - name: Install towncrier +# run: | +# python3 -m pip install towncrier +# - name: Run towncrier +# # Fetch the pull request' base branch so towncrier will be able to +# # compare the current branch with the base branch. +# # Source: https://github.com/actions/checkout/#fetch-all-branches. +# run: | +# git fetch --no-tags origin +refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH} +# towncrier check --compare-with origin/${BASE_BRANCH} +# env: +# BASE_BRANCH: ${{ github.base_ref }} diff --git a/CHANGES.md b/CHANGES.md index b66e567f5b..fd78aa0f96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,42 @@ +Changes in Element v1.4.28 (2022-07-13) +======================================= + +Features ✨ +---------- + - Improve user experience when he is first invited to a room. Users will be able to decrypt and view previous messages ([#5853](https://github.com/vector-im/element-android/issues/5853)) + - [Location sharing] - Reply action on a live message ([#6401](https://github.com/vector-im/element-android/issues/6401)) + - Show a loader if all the Room Members are not yet loaded. ([#6413](https://github.com/vector-im/element-android/issues/6413)) + +Bugfixes 🐛 +---------- + - Fixes numbered lists always starting from 1 ([#4777](https://github.com/vector-im/element-android/issues/4777)) + - Adds LoginType to SessionParams to fix soft logout form not showing for SSO and Password type ([#5398](https://github.com/vector-im/element-android/issues/5398)) + - Use stable endpoint for alias management instead of MSC2432. Contributed by Nico. ([#6288](https://github.com/vector-im/element-android/issues/6288)) + - [Poll] Fixes visible and wrong votes in closed poll after removing 2 previous polls ([#6430](https://github.com/vector-im/element-android/issues/6430)) + - Fix HTML entities being displayed in messages ([#6442](https://github.com/vector-im/element-android/issues/6442)) + - Gallery picker can pick external images ([#6450](https://github.com/vector-im/element-android/issues/6450)) + - Fixes crash when sharing plain text, such as a url ([#6451](https://github.com/vector-im/element-android/issues/6451)) + - Fix crashes on Timeline [Thread] due to range validation ([#6461](https://github.com/vector-im/element-android/issues/6461)) + - Fix crashes when opening Thread ([#6463](https://github.com/vector-im/element-android/issues/6463)) + - Fix ConcurrentModificationException on BackgroundDetectionObserver ([#6469](https://github.com/vector-im/element-android/issues/6469)) + - Fixes inconsistency with rooms within spaces showing or disappearing from home ([#6510](https://github.com/vector-im/element-android/issues/6510)) + +In development 🚧 +---------------- + - FTUE - Adds support for resetting the password during the FTUE onboarding journey ([#5284](https://github.com/vector-im/element-android/issues/5284)) + - Create DM room only on first message - Design implementation & debug feature flag ([#5525](https://github.com/vector-im/element-android/issues/5525)) + +Other changes +------------- + - Replacing Epoxy annotation layout id references with getDefaultLayoutId ([#6389](https://github.com/vector-im/element-android/issues/6389)) + - Ensure `RealmList.clearWith()` extension is correctly used. ([#6392](https://github.com/vector-im/element-android/issues/6392)) + - [Poll] - Add a description under undisclosed poll when not ended ([#6423](https://github.com/vector-im/element-android/issues/6423)) + - Add `android:hasFragileUserData="true"` in the manifest ([#6429](https://github.com/vector-im/element-android/issues/6429)) + - Add code check to prevent modification of frozen class ([#6434](https://github.com/vector-im/element-android/issues/6434)) + - Let your Activity or Fragment implement `VectorMenuProvider` if they provide a menu. ([#6436](https://github.com/vector-im/element-android/issues/6436)) + - Rename Android Service to use `AndroidService` suffix ([#6458](https://github.com/vector-im/element-android/issues/6458)) + + Changes in Element v1.4.27 (2022-07-06) ======================================= diff --git a/build.gradle b/build.gradle index 0244080ad0..61027a0bff 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ buildscript { classpath libs.gradle.gradlePlugin classpath libs.gradle.kotlinPlugin classpath libs.gradle.hiltPlugin - classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.gms:google-services:4.3.13' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.1.1" diff --git a/coverage.gradle b/coverage.gradle index f278a475ef..f335ed8063 100644 --- a/coverage.gradle +++ b/coverage.gradle @@ -7,6 +7,7 @@ def excludes = [ '**/*Activity*', '**/*Fragment*', '**/*Application*', +'**/*AndroidService*', // We would like to exclude android widgets as well but our naming is inconsistent diff --git a/dependencies.gradle b/dependencies.gradle index 6f034eddd8..6588c5205d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -19,9 +19,9 @@ def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" def moshi = "1.13.0" -def lifecycle = "2.4.1" +def lifecycle = "2.5.0" def flowBinding = "1.2.0" -def flipper = "0.151.1" +def flipper = "0.153.0" def epoxy = "4.6.2" def mavericks = "2.7.0" def glide = "4.13.2" @@ -29,7 +29,7 @@ def bigImageViewer = "1.8.1" def jjwt = "0.11.5" def vanniktechEmoji = "0.15.0" -def fragment = "1.4.1" +def fragment = "1.5.0" // 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 +51,7 @@ ext.libs = [ ], androidx : [ 'annotation' : "androidx.annotation:annotation:1.4.0", - 'activity' : "androidx.activity:activity:1.4.0", + 'activity' : "androidx.activity:activity:1.5.0", 'annotations' : "androidx.annotation:annotation:1.3.0", 'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'biometric' : "androidx.biometric:biometric:1.1.0", @@ -97,6 +97,9 @@ ext.libs = [ 'flipper' : "com.facebook.flipper:flipper:$flipper", 'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper", ], + element : [ + 'opusencoder' : "io.element.android:opusencoder:1.0.3", + ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index b8722d3a29..1445d2bb05 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -125,6 +125,7 @@ ext.groups = [ 'commons-logging', 'info.picocli', 'io.arrow-kt', + 'io.element.android', 'io.github.detekt.sarif4k', 'io.github.microutils', 'io.github.reactivecircus.flowbinding', diff --git a/docs/pull_request.md b/docs/pull_request.md index eebf2814a9..7f6ac86b8a 100644 --- a/docs/pull_request.md +++ b/docs/pull_request.md @@ -191,7 +191,7 @@ Examples of prefixes: - `[Bugfix]` - etc. -Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a string requirement. We prefer to spend time to add labels on issues. +Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a strong requirement. We prefer to spend time to add labels on issues. ##### PR description diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000000..7fea7afdd5 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,64 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android test + +```sh +[bundle exec] fastlane android test +``` + +Runs all the tests + +### android beta + +```sh +[bundle exec] fastlane android beta +``` + +Submit a new Beta Build to Crashlytics Beta + +### android deploy + +```sh +[bundle exec] fastlane android deploy +``` + +Deploy a new version to the Google Play + +### android deployMeta + +```sh +[bundle exec] fastlane android deployMeta +``` + +Deploy Google Play metadata + +### android getVersionCode + +```sh +[bundle exec] fastlane android getVersionCode +``` + +Get version code + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104220.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104220.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104230.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104230.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104240.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104240.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104250.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104250.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104140.txt b/fastlane/metadata/android/de-DE/changelogs/40104140.txt new file mode 100644 index 0000000000..3915434ee0 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verbesserte Verwaltung der ignorierten Benutzer:innen. Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104160.txt b/fastlane/metadata/android/de-DE/changelogs/40104160.txt new file mode 100644 index 0000000000..cdf06e217a --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104160.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Bessere Verwaltung von verschlüsselten Nachrichten. Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104180.txt b/fastlane/metadata/android/de-DE/changelogs/40104180.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104190.txt b/fastlane/metadata/android/de-DE/changelogs/40104190.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104200.txt b/fastlane/metadata/android/de-DE/changelogs/40104200.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104220.txt b/fastlane/metadata/android/de-DE/changelogs/40104220.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104230.txt b/fastlane/metadata/android/de-DE/changelogs/40104230.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104240.txt b/fastlane/metadata/android/de-DE/changelogs/40104240.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40104250.txt b/fastlane/metadata/android/de-DE/changelogs/40104250.txt new file mode 100644 index 0000000000..50b5647608 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Verschiedene Fehlerbehebungen und Stabilitätsverbesserungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40104280.txt b/fastlane/metadata/android/en-US/changelogs/40104280.txt new file mode 100644 index 0000000000..61db61727a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104280.txt @@ -0,0 +1,2 @@ +Main changes in this version: Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104220.txt b/fastlane/metadata/android/et/changelogs/40104220.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104230.txt b/fastlane/metadata/android/et/changelogs/40104230.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104240.txt b/fastlane/metadata/android/et/changelogs/40104240.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104250.txt b/fastlane/metadata/android/et/changelogs/40104250.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104220.txt b/fastlane/metadata/android/fa/changelogs/40104220.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104220.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104230.txt b/fastlane/metadata/android/fa/changelogs/40104230.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104230.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104240.txt b/fastlane/metadata/android/fa/changelogs/40104240.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104240.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104250.txt b/fastlane/metadata/android/fa/changelogs/40104250.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104250.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104220.txt b/fastlane/metadata/android/fr-FR/changelogs/40104220.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104230.txt b/fastlane/metadata/android/fr-FR/changelogs/40104230.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104240.txt b/fastlane/metadata/android/fr-FR/changelogs/40104240.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104250.txt b/fastlane/metadata/android/fr-FR/changelogs/40104250.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt index c463dea393..907f907f99 100644 --- a/fastlane/metadata/android/hu-HU/title.txt +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -1 +1 @@ -Element - Biztonságos üzenetküldő +Element diff --git a/fastlane/metadata/android/id/changelogs/40104220.txt b/fastlane/metadata/android/id/changelogs/40104220.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104230.txt b/fastlane/metadata/android/id/changelogs/40104230.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104240.txt b/fastlane/metadata/android/id/changelogs/40104240.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104250.txt b/fastlane/metadata/android/id/changelogs/40104250.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104220.txt b/fastlane/metadata/android/it-IT/changelogs/40104220.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104230.txt b/fastlane/metadata/android/it-IT/changelogs/40104230.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104240.txt b/fastlane/metadata/android/it-IT/changelogs/40104240.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104250.txt b/fastlane/metadata/android/it-IT/changelogs/40104250.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104220.txt b/fastlane/metadata/android/pt-BR/changelogs/40104220.txt new file mode 100644 index 0000000000..5c0d610ddd --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Várias correções de bugs e melhorias de estabilidade. +Registro de mudanças completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104230.txt b/fastlane/metadata/android/pt-BR/changelogs/40104230.txt new file mode 100644 index 0000000000..5c0d610ddd --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Várias correções de bugs e melhorias de estabilidade. +Registro de mudanças completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104240.txt b/fastlane/metadata/android/pt-BR/changelogs/40104240.txt new file mode 100644 index 0000000000..5c0d610ddd --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Várias correções de bugs e melhorias de estabilidade. +Registro de mudanças completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104250.txt b/fastlane/metadata/android/pt-BR/changelogs/40104250.txt new file mode 100644 index 0000000000..5c0d610ddd --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Várias correções de bugs e melhorias de estabilidade. +Registro de mudanças completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104040.txt b/fastlane/metadata/android/ru-RU/changelogs/40104040.txt new file mode 100644 index 0000000000..00904bc548 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104040.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: обновлён индикатор набора сообщения. Исправлены известные ошибки и улучшена стабильность. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.4 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104060.txt b/fastlane/metadata/android/ru-RU/changelogs/40104060.txt new file mode 100644 index 0000000000..e241853529 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104060.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Лента веток работает в реальном времени и быстрее. Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.6 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104070.txt b/fastlane/metadata/android/ru-RU/changelogs/40104070.txt new file mode 100644 index 0000000000..d53e18c58d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104070.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.4.7 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104080.txt b/fastlane/metadata/android/ru-RU/changelogs/40104080.txt new file mode 100644 index 0000000000..faa12f093b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104080.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Лента веток работает в реальном времени и быстрее. Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104100.txt b/fastlane/metadata/android/ru-RU/changelogs/40104100.txt new file mode 100644 index 0000000000..853c7aabec --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Прокрутка голосовых сообщений. Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104110.txt b/fastlane/metadata/android/ru-RU/changelogs/40104110.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104110.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104120.txt b/fastlane/metadata/android/ru-RU/changelogs/40104120.txt new file mode 100644 index 0000000000..0dc35fac76 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104120.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Позволяет пользователям появляться в автономном режиме и добавляет аудиопроигрыватель для аудиовложений +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104130.txt b/fastlane/metadata/android/ru-RU/changelogs/40104130.txt new file mode 100644 index 0000000000..3eec4c4987 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104130.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Позволяет пользователям появляться в автономном режиме и добавляет аудиопроигрыватель для аудиовложений. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104140.txt b/fastlane/metadata/android/ru-RU/changelogs/40104140.txt new file mode 100644 index 0000000000..dcb6d19cbf --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Улучшено управление игнорируемыми пользователями. Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104160.txt b/fastlane/metadata/android/ru-RU/changelogs/40104160.txt new file mode 100644 index 0000000000..ea8f0f84cb --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104160.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Улучшено управление зашифрованными сообщениями. Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104180.txt b/fastlane/metadata/android/ru-RU/changelogs/40104180.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104190.txt b/fastlane/metadata/android/ru-RU/changelogs/40104190.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104200.txt b/fastlane/metadata/android/ru-RU/changelogs/40104200.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104220.txt b/fastlane/metadata/android/ru-RU/changelogs/40104220.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104230.txt b/fastlane/metadata/android/ru-RU/changelogs/40104230.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104240.txt b/fastlane/metadata/android/ru-RU/changelogs/40104240.txt new file mode 100644 index 0000000000..31cc5effa6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Различные исправления ошибок и улучшения стабильности. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104250.txt b/fastlane/metadata/android/ru-RU/changelogs/40104250.txt new file mode 100644 index 0000000000..57c0128d34 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: исправлены известные проблемы и улучшена стабильность. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt index 3d8949e466..b2320d9f0b 100644 --- a/fastlane/metadata/android/ru-RU/full_description.txt +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -40,4 +40,4 @@ Element дает вам возможность контролировать си Открытый исходный код -Element Android - это проект с открытым исходным кодом, размещенный на GitHub. Пожалуйста, сообщайте об ошибках и/или вносите вклад в его развитие по адресу https://github.com/vector-im/element-android. +Element Android - это проект с открытым исходным кодом, размещенный на GitHub. Пожалуйста, сообщайте об ошибках и/или вносите вклад в его развитие по адресу https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/sk/changelogs/40104220.txt b/fastlane/metadata/android/sk/changelogs/40104220.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104230.txt b/fastlane/metadata/android/sk/changelogs/40104230.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104240.txt b/fastlane/metadata/android/sk/changelogs/40104240.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104250.txt b/fastlane/metadata/android/sk/changelogs/40104250.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104220.txt b/fastlane/metadata/android/sv-SE/changelogs/40104220.txt new file mode 100644 index 0000000000..d8db452b51 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104230.txt b/fastlane/metadata/android/sv-SE/changelogs/40104230.txt new file mode 100644 index 0000000000..d8db452b51 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104240.txt b/fastlane/metadata/android/sv-SE/changelogs/40104240.txt new file mode 100644 index 0000000000..d8db452b51 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104250.txt b/fastlane/metadata/android/sv-SE/changelogs/40104250.txt new file mode 100644 index 0000000000..d8db452b51 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ta-IN/title.txt b/fastlane/metadata/android/ta-IN/title.txt index ecb9a01c06..907f907f99 100644 --- a/fastlane/metadata/android/ta-IN/title.txt +++ b/fastlane/metadata/android/ta-IN/title.txt @@ -1 +1 @@ -Element - பாதுகாப்பான தூதுரை சேவை +Element diff --git a/fastlane/metadata/android/uk/changelogs/40104220.txt b/fastlane/metadata/android/uk/changelogs/40104220.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104230.txt b/fastlane/metadata/android/uk/changelogs/40104230.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104240.txt b/fastlane/metadata/android/uk/changelogs/40104240.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104250.txt b/fastlane/metadata/android/uk/changelogs/40104250.txt new file mode 100644 index 0000000000..252a57c9d9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Вичерпний перелік змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104220.txt b/fastlane/metadata/android/zh-TW/changelogs/40104220.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104220.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104230.txt b/fastlane/metadata/android/zh-TW/changelogs/40104230.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104230.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104240.txt b/fastlane/metadata/android/zh-TW/changelogs/40104240.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104240.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104250.txt b/fastlane/metadata/android/zh-TW/changelogs/40104250.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104250.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index e1a3b0c9ee..ad472b0b54 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -2,7 +2,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' -apply plugin: 'com.jakewharton.butterknife' buildscript { repositories { @@ -15,9 +14,6 @@ buildscript { url 'https://repo1.maven.org/maven2' } } - dependencies { - classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' - } } android { diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt index 66dfcc5dc3..590df07f4d 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt @@ -29,7 +29,7 @@ import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence -@EpoxyModelClass(layout = R2.layout.item_jv_base_value) +@EpoxyModelClass internal abstract class ValueItem : EpoxyModelWithHolder() { @EpoxyAttribute @@ -44,6 +44,8 @@ internal abstract class ValueItem : EpoxyModelWithHolder() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null + override fun getDefaultLayout() = R.layout.item_jv_base_value + override fun bind(holder: Holder) { super.bind(holder) holder.textView.text = text?.charSequence diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt index db74dbf9ff..82d0e358df 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt @@ -49,7 +49,7 @@ class MediaPicker : Picker() { return Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) - type = "video/*, image/*" + type = "*/*" val mimeTypes = arrayOf("image/*", "video/*") putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) } diff --git a/library/opusencoder/.gitignore b/library/opusencoder/.gitignore deleted file mode 100644 index ff535c85f5..0000000000 --- a/library/opusencoder/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea -.DS_Store -/build -/captures -*.cxx -app/.cxx/* -.externalNativeBuild \ No newline at end of file diff --git a/library/opusencoder/build.gradle b/library/opusencoder/build.gradle deleted file mode 100644 index a825bb98bc..0000000000 --- a/library/opusencoder/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - ndkVersion "21.3.6528147" - - compileSdkVersion 31 - - buildToolsVersion "31.0.0" - - defaultConfig { - minSdkVersion 18 - targetSdkVersion 31 - versionCode 1 - versionName "1.0" - - externalNativeBuild { - ndk { - abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - } - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -dependencies { - implementation libs.androidx.annotation -} diff --git a/library/opusencoder/src/main/AndroidManifest.xml b/library/opusencoder/src/main/AndroidManifest.xml deleted file mode 100644 index 4dd3413820..0000000000 --- a/library/opusencoder/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/library/opusencoder/src/main/cpp/CMakeLists.txt b/library/opusencoder/src/main/cpp/CMakeLists.txt deleted file mode 100644 index c261af8b90..0000000000 --- a/library/opusencoder/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,61 +0,0 @@ -# For more information about using CMake with Android Studio, read the -# documentation: https://d.android.com/studio/projects/add-native-code.html - -# Sets the minimum version of CMake required to build the native library. - -cmake_minimum_required(VERSION 3.4.1) -set(CMAKE_CXX_STANDARD 14) - -# Creates and names a library, sets it as either STATIC -# or SHARED, and provides the relative paths to its source code. -# You can define multiple libraries, and CMake builds them for you. -# Gradle automatically packages shared libraries with your APK. - -set(distribution_OPUS_DIR ${CMAKE_SOURCE_DIR}/opus) - -add_library(lib_opus SHARED IMPORTED) -set_target_properties(lib_opus PROPERTIES IMPORTED_LOCATION - ${distribution_OPUS_DIR}/libs/${ANDROID_ABI}/libopus.so) - -add_library(lib_opusenc SHARED IMPORTED) -set_target_properties(lib_opusenc PROPERTIES IMPORTED_LOCATION - ${distribution_OPUS_DIR}/libs/${ANDROID_ABI}/libopusenc.so) - -add_library( # Sets the name of the library. - opuscodec - - # Sets the library as a shared library. - SHARED - - # Provides a relative path to your source file(s). - codec/CodecOggOpus.cpp - opuscodec.cpp) - -target_include_directories(opuscodec PRIVATE - ${distribution_OPUS_DIR}/include) - -# Searches for a specified prebuilt library and stores the path as a -# variable. Because CMake includes system libraries in the search path by -# default, you only need to specify the name of the public NDK library -# you want to add. CMake verifies that the library exists before -# completing its build. - -find_library( # Sets the name of the path variable. - log-lib - - # Specifies the name of the NDK library that - # you want CMake to locate. - log ) - -# Specifies libraries CMake should link to your target library. You -# can link multiple libraries, such as libraries you define in this -# build script, prebuilt third-party libraries, or system libraries. - -target_link_libraries( # Specifies the target library. - opuscodec - android - lib_opusenc - lib_opus - # Links the target library to the log library - # included in the NDK. - ${log-lib} ) diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp deleted file mode 100644 index d167c69cbf..0000000000 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.cpp +++ /dev/null @@ -1,63 +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. - */ - -#include "CodecOggOpus.h" -#include "../utils/Logger.h" - -int ret; - -int CodecOggOpus::encoderInit(char* filePath, int sampleRate) { - // Create default, empty comment header - comments = ope_comments_create(); - // Mono audio - int numChannels = 1; - // Channel Mapping Family 0, used for mono/stereo streams - int family = 0; - // Create encoder to encode PCM chunks and write the result to a file with the OggOpus framing - encoder = ope_encoder_create_file(filePath, comments, sampleRate, numChannels, family, &ret); - if (ret != OPE_OK) { - LOGE(TAG, "Creation of OggOpusEnc failed."); - return ret; - } - return OPE_OK; -} - -int CodecOggOpus::setBitrate(int bitrate) { - ret = ope_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST, bitrate); - if (ret != OPE_OK) { - LOGE(TAG, "Could not set bitrate."); - return ret; - } - return OPE_OK; -} - -int CodecOggOpus::writeFrame(short* frame, int samplesPerChannel) { - // Encode the raw PCM-16 buffer to Opus and write it to the ogg file - return ope_encoder_write(encoder, frame, samplesPerChannel); -} - -void CodecOggOpus::encoderRelease() { - // Finish any pending encode/write operations - ope_encoder_drain(encoder); - // De-init the encoder instance - ope_encoder_destroy(encoder); - // De-init the comment header struct - ope_comments_destroy(comments); -} - -CodecOggOpus::~CodecOggOpus() { - encoderRelease(); -} diff --git a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h b/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h deleted file mode 100644 index f1035434c5..0000000000 --- a/library/opusencoder/src/main/cpp/codec/CodecOggOpus.h +++ /dev/null @@ -1,52 +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. - */ - -#ifndef ELEMENT_ANDROID_CODECOGGOPUS_H -#define ELEMENT_ANDROID_CODECOGGOPUS_H - -#include -/** - * This class is a wrapper around libopusenc, used to encode and write Opus frames into an Ogg file. - * - * The usual flow would be: - * - * 1. Use encoderInit to initialize the internal encoder with the sample rate and the path to write the encoded frames to. - * 2. (Optional) set the bitrate to use. - * 3. While recording, read PCM-16 chunks from the recorder, feed them to the encoder using writeFrame. - * 4. When finished, call encoderRelease to free some resources. - */ -class CodecOggOpus { - -private: - const char *TAG = "CodecOggOpus"; - - OggOpusEnc* encoder; - OggOpusComments* comments; - -public: - int encoderInit(char* filePath, int sampleRate); - - int setBitrate(int bitrate); - - int writeFrame(short *frame, int samplesPerChannel); - - void encoderRelease(); - - ~CodecOggOpus(); -}; - - -#endif //ELEMENT_ANDROID_CODECOGGOPUS_H diff --git a/library/opusencoder/src/main/cpp/opus/include/opus.h b/library/opusencoder/src/main/cpp/opus/include/opus.h deleted file mode 100644 index d282f21d25..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus.h +++ /dev/null @@ -1,981 +0,0 @@ -/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited - Written by Jean-Marc Valin and Koen Vos */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - * @file opus.h - * @brief Opus reference implementation API - */ - -#ifndef OPUS_H -#define OPUS_H - -#include "opus_types.h" -#include "opus_defines.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @mainpage Opus - * - * The Opus codec is designed for interactive speech and audio transmission over the Internet. - * It is designed by the IETF Codec Working Group and incorporates technology from - * Skype's SILK codec and Xiph.Org's CELT codec. - * - * The Opus codec is designed to handle a wide range of interactive audio applications, - * including Voice over IP, videoconferencing, in-game chat, and even remote live music - * performances. It can scale from low bit-rate narrowband speech to very high quality - * stereo music. Its main features are: - - * @li Sampling rates from 8 to 48 kHz - * @li Bit-rates from 6 kb/s to 510 kb/s - * @li Support for both constant bit-rate (CBR) and variable bit-rate (VBR) - * @li Audio bandwidth from narrowband to full-band - * @li Support for speech and music - * @li Support for mono and stereo - * @li Support for multichannel (up to 255 channels) - * @li Frame sizes from 2.5 ms to 60 ms - * @li Good loss robustness and packet loss concealment (PLC) - * @li Floating point and fixed-point implementation - * - * Documentation sections: - * @li @ref opus_encoder - * @li @ref opus_decoder - * @li @ref opus_repacketizer - * @li @ref opus_multistream - * @li @ref opus_libinfo - * @li @ref opus_custom - */ - -/** @defgroup opus_encoder Opus Encoder - * @{ - * - * @brief This page describes the process and functions used to encode Opus. - * - * Since Opus is a stateful codec, the encoding process starts with creating an encoder - * state. This can be done with: - * - * @code - * int error; - * OpusEncoder *enc; - * enc = opus_encoder_create(Fs, channels, application, &error); - * @endcode - * - * From this point, @c enc can be used for encoding an audio stream. An encoder state - * @b must @b not be used for more than one stream at the same time. Similarly, the encoder - * state @b must @b not be re-initialized for each frame. - * - * While opus_encoder_create() allocates memory for the state, it's also possible - * to initialize pre-allocated memory: - * - * @code - * int size; - * int error; - * OpusEncoder *enc; - * size = opus_encoder_get_size(channels); - * enc = malloc(size); - * error = opus_encoder_init(enc, Fs, channels, application); - * @endcode - * - * where opus_encoder_get_size() returns the required size for the encoder state. Note that - * future versions of this code may change the size, so no assuptions should be made about it. - * - * The encoder state is always continuous in memory and only a shallow copy is sufficient - * to copy it (e.g. memcpy()) - * - * It is possible to change some of the encoder's settings using the opus_encoder_ctl() - * interface. All these settings already default to the recommended value, so they should - * only be changed when necessary. The most common settings one may want to change are: - * - * @code - * opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate)); - * opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity)); - * opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type)); - * @endcode - * - * where - * - * @arg bitrate is in bits per second (b/s) - * @arg complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest - * @arg signal_type is either OPUS_AUTO (default), OPUS_SIGNAL_VOICE, or OPUS_SIGNAL_MUSIC - * - * See @ref opus_encoderctls and @ref opus_genericctls for a complete list of parameters that can be set or queried. Most parameters can be set or changed at any time during a stream. - * - * To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data: - * @code - * len = opus_encode(enc, audio_frame, frame_size, packet, max_packet); - * @endcode - * - * where - *
    - *
  • audio_frame is the audio data in opus_int16 (or float for opus_encode_float())
  • - *
  • frame_size is the duration of the frame in samples (per channel)
  • - *
  • packet is the byte array to which the compressed data is written
  • - *
  • max_packet is the maximum number of bytes that can be written in the packet (4000 bytes is recommended). - * Do not use max_packet to control VBR target bitrate, instead use the #OPUS_SET_BITRATE CTL.
  • - *
- * - * opus_encode() and opus_encode_float() return the number of bytes actually written to the packet. - * The return value can be negative, which indicates that an error has occurred. If the return value - * is 2 bytes or less, then the packet does not need to be transmitted (DTX). - * - * Once the encoder state if no longer needed, it can be destroyed with - * - * @code - * opus_encoder_destroy(enc); - * @endcode - * - * If the encoder was created with opus_encoder_init() rather than opus_encoder_create(), - * then no action is required aside from potentially freeing the memory that was manually - * allocated for it (calling free(enc) for the example above) - * - */ - -/** Opus encoder state. - * This contains the complete state of an Opus encoder. - * It is position independent and can be freely copied. - * @see opus_encoder_create,opus_encoder_init - */ -typedef struct OpusEncoder OpusEncoder; - -/** Gets the size of an OpusEncoder structure. - * @param[in] channels int: Number of channels. - * This must be 1 or 2. - * @returns The size in bytes. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); - -/** - */ - -/** Allocates and initializes an encoder state. - * There are three coding modes: - * - * @ref OPUS_APPLICATION_VOIP gives best quality at a given bitrate for voice - * signals. It enhances the input signal by high-pass filtering and - * emphasizing formants and harmonics. Optionally it includes in-band - * forward error correction to protect against packet loss. Use this - * mode for typical VoIP applications. Because of the enhancement, - * even at high bitrates the output may sound different from the input. - * - * @ref OPUS_APPLICATION_AUDIO gives best quality at a given bitrate for most - * non-voice signals like music. Use this mode for music and mixed - * (music/voice) content, broadcast, and applications requiring less - * than 15 ms of coding delay. - * - * @ref OPUS_APPLICATION_RESTRICTED_LOWDELAY configures low-delay mode that - * disables the speech-optimized mode in exchange for slightly reduced delay. - * This mode can only be set on an newly initialized or freshly reset encoder - * because it changes the codec delay. - * - * This is useful when the caller knows that the speech-optimized modes will not be needed (use with caution). - * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) - * @param [out] error int*: @ref opus_errorcodes - * @note Regardless of the sampling rate and number channels selected, the Opus encoder - * can switch to a lower audio bandwidth or number of channels if the bitrate - * selected is too low. This also means that it is safe to always use 48 kHz stereo input - * and let the encoder optimize the encoding. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( - opus_int32 Fs, - int channels, - int application, - int *error -); - -/** Initializes a previously allocated encoder state - * The memory pointed to by st must be at least the size returned by opus_encoder_get_size(). - * This is intended for applications which use their own allocator instead of malloc. - * @see opus_encoder_create(),opus_encoder_get_size() - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @param [in] st OpusEncoder*: Encoder state - * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) - * @retval #OPUS_OK Success or @ref opus_errorcodes - */ -OPUS_EXPORT int opus_encoder_init( - OpusEncoder *st, - opus_int32 Fs, - int channels, - int application -) OPUS_ARG_NONNULL(1); - -/** Encodes an Opus frame. - * @param [in] st OpusEncoder*: Encoder state - * @param [in] pcm opus_int16*: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16) - * @param [in] frame_size int: Number of samples per channel in the - * input signal. - * This must be an Opus frame size for - * the encoder's sampling rate. - * For example, at 48 kHz the permitted - * values are 120, 240, 480, 960, 1920, - * and 2880. - * Passing in a duration of less than - * 10 ms (480 samples at 48 kHz) will - * prevent the encoder from using the LPC - * or hybrid modes. - * @param [out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode( - OpusEncoder *st, - const opus_int16 *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Encodes an Opus frame from floating point input. - * @param [in] st OpusEncoder*: Encoder state - * @param [in] pcm float*: Input in float format (interleaved if 2 channels), with a normal range of +/-1.0. - * Samples with a range beyond +/-1.0 are supported but will - * be clipped by decoders using the integer API and should - * only be used if it is known that the far end supports - * extended dynamic range. - * length is frame_size*channels*sizeof(float) - * @param [in] frame_size int: Number of samples per channel in the - * input signal. - * This must be an Opus frame size for - * the encoder's sampling rate. - * For example, at 48 kHz the permitted - * values are 120, 240, 480, 960, 1920, - * and 2880. - * Passing in a duration of less than - * 10 ms (480 samples at 48 kHz) will - * prevent the encoder from using the LPC - * or hybrid modes. - * @param [out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode_float( - OpusEncoder *st, - const float *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Frees an OpusEncoder allocated by opus_encoder_create(). - * @param[in] st OpusEncoder*: State to be freed. - */ -OPUS_EXPORT void opus_encoder_destroy(OpusEncoder *st); - -/** Perform a CTL function on an Opus encoder. - * - * Generally the request and subsequent arguments are generated - * by a convenience macro. - * @param st OpusEncoder*: Encoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls or - * @ref opus_encoderctls. - * @see opus_genericctls - * @see opus_encoderctls - */ -OPUS_EXPORT int opus_encoder_ctl(OpusEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); -/**@}*/ - -/** @defgroup opus_decoder Opus Decoder - * @{ - * - * @brief This page describes the process and functions used to decode Opus. - * - * The decoding process also starts with creating a decoder - * state. This can be done with: - * @code - * int error; - * OpusDecoder *dec; - * dec = opus_decoder_create(Fs, channels, &error); - * @endcode - * where - * @li Fs is the sampling rate and must be 8000, 12000, 16000, 24000, or 48000 - * @li channels is the number of channels (1 or 2) - * @li error will hold the error code in case of failure (or #OPUS_OK on success) - * @li the return value is a newly created decoder state to be used for decoding - * - * While opus_decoder_create() allocates memory for the state, it's also possible - * to initialize pre-allocated memory: - * @code - * int size; - * int error; - * OpusDecoder *dec; - * size = opus_decoder_get_size(channels); - * dec = malloc(size); - * error = opus_decoder_init(dec, Fs, channels); - * @endcode - * where opus_decoder_get_size() returns the required size for the decoder state. Note that - * future versions of this code may change the size, so no assuptions should be made about it. - * - * The decoder state is always continuous in memory and only a shallow copy is sufficient - * to copy it (e.g. memcpy()) - * - * To decode a frame, opus_decode() or opus_decode_float() must be called with a packet of compressed audio data: - * @code - * frame_size = opus_decode(dec, packet, len, decoded, max_size, 0); - * @endcode - * where - * - * @li packet is the byte array containing the compressed data - * @li len is the exact number of bytes contained in the packet - * @li decoded is the decoded audio data in opus_int16 (or float for opus_decode_float()) - * @li max_size is the max duration of the frame in samples (per channel) that can fit into the decoded_frame array - * - * opus_decode() and opus_decode_float() return the number of samples (per channel) decoded from the packet. - * If that value is negative, then an error has occurred. This can occur if the packet is corrupted or if the audio - * buffer is too small to hold the decoded audio. - * - * Opus is a stateful codec with overlapping blocks and as a result Opus - * packets are not coded independently of each other. Packets must be - * passed into the decoder serially and in the correct order for a correct - * decode. Lost packets can be replaced with loss concealment by calling - * the decoder with a null pointer and zero length for the missing packet. - * - * A single codec state may only be accessed from a single thread at - * a time and any required locking must be performed by the caller. Separate - * streams must be decoded with separate decoder states and can be decoded - * in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK - * defined. - * - */ - -/** Opus decoder state. - * This contains the complete state of an Opus decoder. - * It is position independent and can be freely copied. - * @see opus_decoder_create,opus_decoder_init - */ -typedef struct OpusDecoder OpusDecoder; - -/** Gets the size of an OpusDecoder structure. - * @param [in] channels int: Number of channels. - * This must be 1 or 2. - * @returns The size in bytes. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_size(int channels); - -/** Allocates and initializes a decoder state. - * @param [in] Fs opus_int32: Sample rate to decode at (Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param [in] channels int: Number of channels (1 or 2) to decode - * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes - * - * Internally Opus stores data at 48000 Hz, so that should be the default - * value for Fs. However, the decoder can efficiently decode to buffers - * at 8, 12, 16, and 24 kHz so if for some reason the caller cannot use - * data at the full sample rate, or knows the compressed data doesn't - * use the full frequency range, it can request decoding at a reduced - * rate. Likewise, the decoder is capable of filling in either mono or - * interleaved stereo pcm buffers, at the caller's request. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusDecoder *opus_decoder_create( - opus_int32 Fs, - int channels, - int *error -); - -/** Initializes a previously allocated decoder state. - * The state must be at least the size returned by opus_decoder_get_size(). - * This is intended for applications which use their own allocator instead of malloc. @see opus_decoder_create,opus_decoder_get_size - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @param [in] st OpusDecoder*: Decoder state. - * @param [in] Fs opus_int32: Sampling rate to decode to (Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param [in] channels int: Number of channels (1 or 2) to decode - * @retval #OPUS_OK Success or @ref opus_errorcodes - */ -OPUS_EXPORT int opus_decoder_init( - OpusDecoder *st, - opus_int32 Fs, - int channels -) OPUS_ARG_NONNULL(1); - -/** Decode an Opus packet. - * @param [in] st OpusDecoder*: Decoder state - * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss - * @param [in] len opus_int32: Number of bytes in payload* - * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length - * is frame_size*channels*sizeof(opus_int16) - * @param [in] frame_size Number of samples per channel of available space in \a pcm. - * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will - * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), - * then frame_size needs to be exactly the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and - * FEC cases, frame_size must be a multiple of 2.5 ms. - * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be - * decoded. If no such data is available, the frame is decoded as if it were lost. - * @returns Number of decoded samples or @ref opus_errorcodes - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode( - OpusDecoder *st, - const unsigned char *data, - opus_int32 len, - opus_int16 *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Decode an Opus packet with floating point output. - * @param [in] st OpusDecoder*: Decoder state - * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss - * @param [in] len opus_int32: Number of bytes in payload - * @param [out] pcm float*: Output signal (interleaved if 2 channels). length - * is frame_size*channels*sizeof(float) - * @param [in] frame_size Number of samples per channel of available space in \a pcm. - * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will - * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), - * then frame_size needs to be exactly the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and - * FEC cases, frame_size must be a multiple of 2.5 ms. - * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be - * decoded. If no such data is available the frame is decoded as if it were lost. - * @returns Number of decoded samples or @ref opus_errorcodes - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode_float( - OpusDecoder *st, - const unsigned char *data, - opus_int32 len, - float *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Perform a CTL function on an Opus decoder. - * - * Generally the request and subsequent arguments are generated - * by a convenience macro. - * @param st OpusDecoder*: Decoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls or - * @ref opus_decoderctls. - * @see opus_genericctls - * @see opus_decoderctls - */ -OPUS_EXPORT int opus_decoder_ctl(OpusDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); - -/** Frees an OpusDecoder allocated by opus_decoder_create(). - * @param[in] st OpusDecoder*: State to be freed. - */ -OPUS_EXPORT void opus_decoder_destroy(OpusDecoder *st); - -/** Parse an opus packet into one or more frames. - * Opus_decode will perform this operation internally so most applications do - * not need to use this function. - * This function does not copy the frames, the returned pointers are pointers into - * the input packet. - * @param [in] data char*: Opus packet to be parsed - * @param [in] len opus_int32: size of data - * @param [out] out_toc char*: TOC pointer - * @param [out] frames char*[48] encapsulated frames - * @param [out] size opus_int16[48] sizes of the encapsulated frames - * @param [out] payload_offset int*: returns the position of the payload within the packet (in bytes) - * @returns number of frames - */ -OPUS_EXPORT int opus_packet_parse( - const unsigned char *data, - opus_int32 len, - unsigned char *out_toc, - const unsigned char *frames[48], - opus_int16 size[48], - int *payload_offset -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5); - -/** Gets the bandwidth of an Opus packet. - * @param [in] data char*: Opus packet - * @retval OPUS_BANDWIDTH_NARROWBAND Narrowband (4kHz bandpass) - * @retval OPUS_BANDWIDTH_MEDIUMBAND Mediumband (6kHz bandpass) - * @retval OPUS_BANDWIDTH_WIDEBAND Wideband (8kHz bandpass) - * @retval OPUS_BANDWIDTH_SUPERWIDEBAND Superwideband (12kHz bandpass) - * @retval OPUS_BANDWIDTH_FULLBAND Fullband (20kHz bandpass) - * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_bandwidth(const unsigned char *data) OPUS_ARG_NONNULL(1); - -/** Gets the number of samples per frame from an Opus packet. - * @param [in] data char*: Opus packet. - * This must contain at least one byte of - * data. - * @param [in] Fs opus_int32: Sampling rate in Hz. - * This must be a multiple of 400, or - * inaccurate results will be returned. - * @returns Number of samples per frame. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_samples_per_frame(const unsigned char *data, opus_int32 Fs) OPUS_ARG_NONNULL(1); - -/** Gets the number of channels from an Opus packet. - * @param [in] data char*: Opus packet - * @returns Number of channels - * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_channels(const unsigned char *data) OPUS_ARG_NONNULL(1); - -/** Gets the number of frames in an Opus packet. - * @param [in] packet char*: Opus packet - * @param [in] len opus_int32: Length of packet - * @returns Number of frames - * @retval OPUS_BAD_ARG Insufficient data was passed to the function - * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1); - -/** Gets the number of samples of an Opus packet. - * @param [in] packet char*: Opus packet - * @param [in] len opus_int32: Length of packet - * @param [in] Fs opus_int32: Sampling rate in Hz. - * This must be a multiple of 400, or - * inaccurate results will be returned. - * @returns Number of samples - * @retval OPUS_BAD_ARG Insufficient data was passed to the function - * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, opus_int32 Fs) OPUS_ARG_NONNULL(1); - -/** Gets the number of samples of an Opus packet. - * @param [in] dec OpusDecoder*: Decoder state - * @param [in] packet char*: Opus packet - * @param [in] len opus_int32: Length of packet - * @returns Number of samples - * @retval OPUS_BAD_ARG Insufficient data was passed to the function - * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_nb_samples(const OpusDecoder *dec, const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); - -/** Applies soft-clipping to bring a float signal within the [-1,1] range. If - * the signal is already in that range, nothing is done. If there are values - * outside of [-1,1], then the signal is clipped as smoothly as possible to - * both fit in the range and avoid creating excessive distortion in the - * process. - * @param [in,out] pcm float*: Input PCM and modified PCM - * @param [in] frame_size int Number of samples per channel to process - * @param [in] channels int: Number of channels - * @param [in,out] softclip_mem float*: State memory for the soft clipping process (one float per channel, initialized to zero) - */ -OPUS_EXPORT void opus_pcm_soft_clip(float *pcm, int frame_size, int channels, float *softclip_mem); - - -/**@}*/ - -/** @defgroup opus_repacketizer Repacketizer - * @{ - * - * The repacketizer can be used to merge multiple Opus packets into a single - * packet or alternatively to split Opus packets that have previously been - * merged. Splitting valid Opus packets is always guaranteed to succeed, - * whereas merging valid packets only succeeds if all frames have the same - * mode, bandwidth, and frame size, and when the total duration of the merged - * packet is no more than 120 ms. The 120 ms limit comes from the - * specification and limits decoder memory requirements at a point where - * framing overhead becomes negligible. - * - * The repacketizer currently only operates on elementary Opus - * streams. It will not manipualte multistream packets successfully, except in - * the degenerate case where they consist of data from a single stream. - * - * The repacketizing process starts with creating a repacketizer state, either - * by calling opus_repacketizer_create() or by allocating the memory yourself, - * e.g., - * @code - * OpusRepacketizer *rp; - * rp = (OpusRepacketizer*)malloc(opus_repacketizer_get_size()); - * if (rp != NULL) - * opus_repacketizer_init(rp); - * @endcode - * - * Then the application should submit packets with opus_repacketizer_cat(), - * extract new packets with opus_repacketizer_out() or - * opus_repacketizer_out_range(), and then reset the state for the next set of - * input packets via opus_repacketizer_init(). - * - * For example, to split a sequence of packets into individual frames: - * @code - * unsigned char *data; - * int len; - * while (get_next_packet(&data, &len)) - * { - * unsigned char out[1276]; - * opus_int32 out_len; - * int nb_frames; - * int err; - * int i; - * err = opus_repacketizer_cat(rp, data, len); - * if (err != OPUS_OK) - * { - * release_packet(data); - * return err; - * } - * nb_frames = opus_repacketizer_get_nb_frames(rp); - * for (i = 0; i < nb_frames; i++) - * { - * out_len = opus_repacketizer_out_range(rp, i, i+1, out, sizeof(out)); - * if (out_len < 0) - * { - * release_packet(data); - * return (int)out_len; - * } - * output_next_packet(out, out_len); - * } - * opus_repacketizer_init(rp); - * release_packet(data); - * } - * @endcode - * - * Alternatively, to combine a sequence of frames into packets that each - * contain up to TARGET_DURATION_MS milliseconds of data: - * @code - * // The maximum number of packets with duration TARGET_DURATION_MS occurs - * // when the frame size is 2.5 ms, for a total of (TARGET_DURATION_MS*2/5) - * // packets. - * unsigned char *data[(TARGET_DURATION_MS*2/5)+1]; - * opus_int32 len[(TARGET_DURATION_MS*2/5)+1]; - * int nb_packets; - * unsigned char out[1277*(TARGET_DURATION_MS*2/2)]; - * opus_int32 out_len; - * int prev_toc; - * nb_packets = 0; - * while (get_next_packet(data+nb_packets, len+nb_packets)) - * { - * int nb_frames; - * int err; - * nb_frames = opus_packet_get_nb_frames(data[nb_packets], len[nb_packets]); - * if (nb_frames < 1) - * { - * release_packets(data, nb_packets+1); - * return nb_frames; - * } - * nb_frames += opus_repacketizer_get_nb_frames(rp); - * // If adding the next packet would exceed our target, or it has an - * // incompatible TOC sequence, output the packets we already have before - * // submitting it. - * // N.B., The nb_packets > 0 check ensures we've submitted at least one - * // packet since the last call to opus_repacketizer_init(). Otherwise a - * // single packet longer than TARGET_DURATION_MS would cause us to try to - * // output an (invalid) empty packet. It also ensures that prev_toc has - * // been set to a valid value. Additionally, len[nb_packets] > 0 is - * // guaranteed by the call to opus_packet_get_nb_frames() above, so the - * // reference to data[nb_packets][0] should be valid. - * if (nb_packets > 0 && ( - * ((prev_toc & 0xFC) != (data[nb_packets][0] & 0xFC)) || - * opus_packet_get_samples_per_frame(data[nb_packets], 48000)*nb_frames > - * TARGET_DURATION_MS*48)) - * { - * out_len = opus_repacketizer_out(rp, out, sizeof(out)); - * if (out_len < 0) - * { - * release_packets(data, nb_packets+1); - * return (int)out_len; - * } - * output_next_packet(out, out_len); - * opus_repacketizer_init(rp); - * release_packets(data, nb_packets); - * data[0] = data[nb_packets]; - * len[0] = len[nb_packets]; - * nb_packets = 0; - * } - * err = opus_repacketizer_cat(rp, data[nb_packets], len[nb_packets]); - * if (err != OPUS_OK) - * { - * release_packets(data, nb_packets+1); - * return err; - * } - * prev_toc = data[nb_packets][0]; - * nb_packets++; - * } - * // Output the final, partial packet. - * if (nb_packets > 0) - * { - * out_len = opus_repacketizer_out(rp, out, sizeof(out)); - * release_packets(data, nb_packets); - * if (out_len < 0) - * return (int)out_len; - * output_next_packet(out, out_len); - * } - * @endcode - * - * An alternate way of merging packets is to simply call opus_repacketizer_cat() - * unconditionally until it fails. At that point, the merged packet can be - * obtained with opus_repacketizer_out() and the input packet for which - * opus_repacketizer_cat() needs to be re-added to a newly reinitialized - * repacketizer state. - */ - -typedef struct OpusRepacketizer OpusRepacketizer; - -/** Gets the size of an OpusRepacketizer structure. - * @returns The size in bytes. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_size(void); - -/** (Re)initializes a previously allocated repacketizer state. - * The state must be at least the size returned by opus_repacketizer_get_size(). - * This can be used for applications which use their own allocator instead of - * malloc(). - * It must also be called to reset the queue of packets waiting to be - * repacketized, which is necessary if the maximum packet duration of 120 ms - * is reached or if you wish to submit packets with a different Opus - * configuration (coding mode, audio bandwidth, frame size, or channel count). - * Failure to do so will prevent a new packet from being added with - * opus_repacketizer_cat(). - * @see opus_repacketizer_create - * @see opus_repacketizer_get_size - * @see opus_repacketizer_cat - * @param rp OpusRepacketizer*: The repacketizer state to - * (re)initialize. - * @returns A pointer to the same repacketizer state that was passed in. - */ -OPUS_EXPORT OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); - -/** Allocates memory and initializes the new repacketizer with - * opus_repacketizer_init(). - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusRepacketizer *opus_repacketizer_create(void); - -/** Frees an OpusRepacketizer allocated by - * opus_repacketizer_create(). - * @param[in] rp OpusRepacketizer*: State to be freed. - */ -OPUS_EXPORT void opus_repacketizer_destroy(OpusRepacketizer *rp); - -/** Add a packet to the current repacketizer state. - * This packet must match the configuration of any packets already submitted - * for repacketization since the last call to opus_repacketizer_init(). - * This means that it must have the same coding mode, audio bandwidth, frame - * size, and channel count. - * This can be checked in advance by examining the top 6 bits of the first - * byte of the packet, and ensuring they match the top 6 bits of the first - * byte of any previously submitted packet. - * The total duration of audio in the repacketizer state also must not exceed - * 120 ms, the maximum duration of a single packet, after adding this packet. - * - * The contents of the current repacketizer state can be extracted into new - * packets using opus_repacketizer_out() or opus_repacketizer_out_range(). - * - * In order to add a packet with a different configuration or to add more - * audio beyond 120 ms, you must clear the repacketizer state by calling - * opus_repacketizer_init(). - * If a packet is too large to add to the current repacketizer state, no part - * of it is added, even if it contains multiple frames, some of which might - * fit. - * If you wish to be able to add parts of such packets, you should first use - * another repacketizer to split the packet into pieces and add them - * individually. - * @see opus_repacketizer_out_range - * @see opus_repacketizer_out - * @see opus_repacketizer_init - * @param rp OpusRepacketizer*: The repacketizer state to which to - * add the packet. - * @param[in] data const unsigned char*: The packet data. - * The application must ensure - * this pointer remains valid - * until the next call to - * opus_repacketizer_init() or - * opus_repacketizer_destroy(). - * @param len opus_int32: The number of bytes in the packet data. - * @returns An error code indicating whether or not the operation succeeded. - * @retval #OPUS_OK The packet's contents have been added to the repacketizer - * state. - * @retval #OPUS_INVALID_PACKET The packet did not have a valid TOC sequence, - * the packet's TOC sequence was not compatible - * with previously submitted packets (because - * the coding mode, audio bandwidth, frame size, - * or channel count did not match), or adding - * this packet would increase the total amount of - * audio stored in the repacketizer state to more - * than 120 ms. - */ -OPUS_EXPORT int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); - - -/** Construct a new packet from data previously submitted to the repacketizer - * state via opus_repacketizer_cat(). - * @param rp OpusRepacketizer*: The repacketizer state from which to - * construct the new packet. - * @param begin int: The index of the first frame in the current - * repacketizer state to include in the output. - * @param end int: One past the index of the last frame in the - * current repacketizer state to include in the - * output. - * @param[out] data const unsigned char*: The buffer in which to - * store the output packet. - * @param maxlen opus_int32: The maximum number of bytes to store in - * the output buffer. In order to guarantee - * success, this should be at least - * 1276 for a single frame, - * or for multiple frames, - * 1277*(end-begin). - * However, 1*(end-begin) plus - * the size of all packet data submitted to - * the repacketizer since the last call to - * opus_repacketizer_init() or - * opus_repacketizer_create() is also - * sufficient, and possibly much smaller. - * @returns The total size of the output packet on success, or an error code - * on failure. - * @retval #OPUS_BAD_ARG [begin,end) was an invalid range of - * frames (begin < 0, begin >= end, or end > - * opus_repacketizer_get_nb_frames()). - * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the - * complete output packet. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out_range(OpusRepacketizer *rp, int begin, int end, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Return the total number of frames contained in packet data submitted to - * the repacketizer state so far via opus_repacketizer_cat() since the last - * call to opus_repacketizer_init() or opus_repacketizer_create(). - * This defines the valid range of packets that can be extracted with - * opus_repacketizer_out_range() or opus_repacketizer_out(). - * @param rp OpusRepacketizer*: The repacketizer state containing the - * frames. - * @returns The total number of frames contained in the packet data submitted - * to the repacketizer state. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); - -/** Construct a new packet from data previously submitted to the repacketizer - * state via opus_repacketizer_cat(). - * This is a convenience routine that returns all the data submitted so far - * in a single packet. - * It is equivalent to calling - * @code - * opus_repacketizer_out_range(rp, 0, opus_repacketizer_get_nb_frames(rp), - * data, maxlen) - * @endcode - * @param rp OpusRepacketizer*: The repacketizer state from which to - * construct the new packet. - * @param[out] data const unsigned char*: The buffer in which to - * store the output packet. - * @param maxlen opus_int32: The maximum number of bytes to store in - * the output buffer. In order to guarantee - * success, this should be at least - * 1277*opus_repacketizer_get_nb_frames(rp). - * However, - * 1*opus_repacketizer_get_nb_frames(rp) - * plus the size of all packet data - * submitted to the repacketizer since the - * last call to opus_repacketizer_init() or - * opus_repacketizer_create() is also - * sufficient, and possibly much smaller. - * @returns The total size of the output packet on success, or an error code - * on failure. - * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the - * complete output packet. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out(OpusRepacketizer *rp, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1); - -/** Pads a given Opus packet to a larger size (possibly changing the TOC sequence). - * @param[in,out] data const unsigned char*: The buffer containing the - * packet to pad. - * @param len opus_int32: The size of the packet. - * This must be at least 1. - * @param new_len opus_int32: The desired size of the packet after padding. - * This must be at least as large as len. - * @returns an error code - * @retval #OPUS_OK \a on success. - * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. - * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. - */ -OPUS_EXPORT int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len); - -/** Remove all padding from a given Opus packet and rewrite the TOC sequence to - * minimize space usage. - * @param[in,out] data const unsigned char*: The buffer containing the - * packet to strip. - * @param len opus_int32: The size of the packet. - * This must be at least 1. - * @returns The new size of the output packet on success, or an error code - * on failure. - * @retval #OPUS_BAD_ARG \a len was less than 1. - * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len); - -/** Pads a given Opus multi-stream packet to a larger size (possibly changing the TOC sequence). - * @param[in,out] data const unsigned char*: The buffer containing the - * packet to pad. - * @param len opus_int32: The size of the packet. - * This must be at least 1. - * @param new_len opus_int32: The desired size of the packet after padding. - * This must be at least 1. - * @param nb_streams opus_int32: The number of streams (not channels) in the packet. - * This must be at least as large as len. - * @returns an error code - * @retval #OPUS_OK \a on success. - * @retval #OPUS_BAD_ARG \a len was less than 1. - * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. - */ -OPUS_EXPORT int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams); - -/** Remove all padding from a given Opus multi-stream packet and rewrite the TOC sequence to - * minimize space usage. - * @param[in,out] data const unsigned char*: The buffer containing the - * packet to strip. - * @param len opus_int32: The size of the packet. - * This must be at least 1. - * @param nb_streams opus_int32: The number of streams (not channels) in the packet. - * This must be at least 1. - * @returns The new size of the output packet on success, or an error code - * on failure. - * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. - * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_packet_unpad(unsigned char *data, opus_int32 len, int nb_streams); - -/**@}*/ - -#ifdef __cplusplus -} -#endif - -#endif /* OPUS_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_custom.h b/library/opusencoder/src/main/cpp/opus/include/opus_custom.h deleted file mode 100644 index 41f36bf2fb..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus_custom.h +++ /dev/null @@ -1,342 +0,0 @@ -/* Copyright (c) 2007-2008 CSIRO - Copyright (c) 2007-2009 Xiph.Org Foundation - Copyright (c) 2008-2012 Gregory Maxwell - Written by Jean-Marc Valin and Gregory Maxwell */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - @file opus_custom.h - @brief Opus-Custom reference implementation API - */ - -#ifndef OPUS_CUSTOM_H -#define OPUS_CUSTOM_H - -#include "opus_defines.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef CUSTOM_MODES -# define OPUS_CUSTOM_EXPORT OPUS_EXPORT -# define OPUS_CUSTOM_EXPORT_STATIC OPUS_EXPORT -#else -# define OPUS_CUSTOM_EXPORT -# ifdef OPUS_BUILD -# define OPUS_CUSTOM_EXPORT_STATIC static OPUS_INLINE -# else -# define OPUS_CUSTOM_EXPORT_STATIC -# endif -#endif - -/** @defgroup opus_custom Opus Custom - * @{ - * Opus Custom is an optional part of the Opus specification and - * reference implementation which uses a distinct API from the regular - * API and supports frame sizes that are not normally supported.\ Use - * of Opus Custom is discouraged for all but very special applications - * for which a frame size different from 2.5, 5, 10, or 20 ms is needed - * (for either complexity or latency reasons) and where interoperability - * is less important. - * - * In addition to the interoperability limitations the use of Opus custom - * disables a substantial chunk of the codec and generally lowers the - * quality available at a given bitrate. Normally when an application needs - * a different frame size from the codec it should buffer to match the - * sizes but this adds a small amount of delay which may be important - * in some very low latency applications. Some transports (especially - * constant rate RF transports) may also work best with frames of - * particular durations. - * - * Libopus only supports custom modes if they are enabled at compile time. - * - * The Opus Custom API is similar to the regular API but the - * @ref opus_encoder_create and @ref opus_decoder_create calls take - * an additional mode parameter which is a structure produced by - * a call to @ref opus_custom_mode_create. Both the encoder and decoder - * must create a mode using the same sample rate (fs) and frame size - * (frame size) so these parameters must either be signaled out of band - * or fixed in a particular implementation. - * - * Similar to regular Opus the custom modes support on the fly frame size - * switching, but the sizes available depend on the particular frame size in - * use. For some initial frame sizes on a single on the fly size is available. - */ - -/** Contains the state of an encoder. One encoder state is needed - for each stream. It is initialized once at the beginning of the - stream. Do *not* re-initialize the state for every frame. - @brief Encoder state - */ -typedef struct OpusCustomEncoder OpusCustomEncoder; - -/** State of the decoder. One decoder state is needed for each stream. - It is initialized once at the beginning of the stream. Do *not* - re-initialize the state for every frame. - @brief Decoder state - */ -typedef struct OpusCustomDecoder OpusCustomDecoder; - -/** The mode contains all the information necessary to create an - encoder. Both the encoder and decoder need to be initialized - with exactly the same mode, otherwise the output will be - corrupted. - @brief Mode configuration - */ -typedef struct OpusCustomMode OpusCustomMode; - -/** Creates a new mode struct. This will be passed to an encoder or - * decoder. The mode MUST NOT BE DESTROYED until the encoders and - * decoders that use it are destroyed as well. - * @param [in] Fs int: Sampling rate (8000 to 96000 Hz) - * @param [in] frame_size int: Number of samples (per channel) to encode in each - * packet (64 - 1024, prime factorization must contain zero or more 2s, 3s, or 5s and no other primes) - * @param [out] error int*: Returned error code (if NULL, no error will be returned) - * @return A newly created mode - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error); - -/** Destroys a mode struct. Only call this after all encoders and - * decoders using this mode are destroyed as well. - * @param [in] mode OpusCustomMode*: Mode to be freed. - */ -OPUS_CUSTOM_EXPORT void opus_custom_mode_destroy(OpusCustomMode *mode); - - -#if !defined(OPUS_BUILD) || defined(CELT_ENCODER_C) - -/* Encoder */ -/** Gets the size of an OpusCustomEncoder structure. - * @param [in] mode OpusCustomMode *: Mode configuration - * @param [in] channels int: Number of channels - * @returns size - */ -OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_encoder_get_size( - const OpusCustomMode *mode, - int channels -) OPUS_ARG_NONNULL(1); - -# ifdef CUSTOM_MODES -/** Initializes a previously allocated encoder state - * The memory pointed to by st must be the size returned by opus_custom_encoder_get_size. - * This is intended for applications which use their own allocator instead of malloc. - * @see opus_custom_encoder_create(),opus_custom_encoder_get_size() - * To reset a previously initialized state use the OPUS_RESET_STATE CTL. - * @param [in] st OpusCustomEncoder*: Encoder state - * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of - * the stream (must be the same characteristics as used for the - * decoder) - * @param [in] channels int: Number of channels - * @return OPUS_OK Success or @ref opus_errorcodes - */ -OPUS_CUSTOM_EXPORT int opus_custom_encoder_init( - OpusCustomEncoder *st, - const OpusCustomMode *mode, - int channels -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); -# endif -#endif - - -/** Creates a new encoder state. Each stream needs its own encoder - * state (can't be shared across simultaneous streams). - * @param [in] mode OpusCustomMode*: Contains all the information about the characteristics of - * the stream (must be the same characteristics as used for the - * decoder) - * @param [in] channels int: Number of channels - * @param [out] error int*: Returns an error code - * @return Newly created encoder state. -*/ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomEncoder *opus_custom_encoder_create( - const OpusCustomMode *mode, - int channels, - int *error -) OPUS_ARG_NONNULL(1); - - -/** Destroys a an encoder state. - * @param[in] st OpusCustomEncoder*: State to be freed. - */ -OPUS_CUSTOM_EXPORT void opus_custom_encoder_destroy(OpusCustomEncoder *st); - -/** Encodes a frame of audio. - * @param [in] st OpusCustomEncoder*: Encoder state - * @param [in] pcm float*: PCM audio in float format, with a normal range of +/-1.0. - * Samples with a range beyond +/-1.0 are supported but will - * be clipped by decoders using the integer API and should - * only be used if it is known that the far end supports - * extended dynamic range. There must be exactly - * frame_size samples per channel. - * @param [in] frame_size int: Number of samples per frame of input signal - * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. - * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame - * (can change from one frame to another) - * @return Number of bytes written to "compressed". - * If negative, an error has occurred (see error codes). It is IMPORTANT that - * the length returned be somehow transmitted to the decoder. Otherwise, no - * decoding is possible. - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode_float( - OpusCustomEncoder *st, - const float *pcm, - int frame_size, - unsigned char *compressed, - int maxCompressedBytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Encodes a frame of audio. - * @param [in] st OpusCustomEncoder*: Encoder state - * @param [in] pcm opus_int16*: PCM audio in signed 16-bit format (native endian). - * There must be exactly frame_size samples per channel. - * @param [in] frame_size int: Number of samples per frame of input signal - * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. - * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame - * (can change from one frame to another) - * @return Number of bytes written to "compressed". - * If negative, an error has occurred (see error codes). It is IMPORTANT that - * the length returned be somehow transmitted to the decoder. Otherwise, no - * decoding is possible. - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode( - OpusCustomEncoder *st, - const opus_int16 *pcm, - int frame_size, - unsigned char *compressed, - int maxCompressedBytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Perform a CTL function on an Opus custom encoder. - * - * Generally the request and subsequent arguments are generated - * by a convenience macro. - * @see opus_encoderctls - */ -OPUS_CUSTOM_EXPORT int opus_custom_encoder_ctl(OpusCustomEncoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); - - -#if !defined(OPUS_BUILD) || defined(CELT_DECODER_C) -/* Decoder */ - -/** Gets the size of an OpusCustomDecoder structure. - * @param [in] mode OpusCustomMode *: Mode configuration - * @param [in] channels int: Number of channels - * @returns size - */ -OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_decoder_get_size( - const OpusCustomMode *mode, - int channels -) OPUS_ARG_NONNULL(1); - -/** Initializes a previously allocated decoder state - * The memory pointed to by st must be the size returned by opus_custom_decoder_get_size. - * This is intended for applications which use their own allocator instead of malloc. - * @see opus_custom_decoder_create(),opus_custom_decoder_get_size() - * To reset a previously initialized state use the OPUS_RESET_STATE CTL. - * @param [in] st OpusCustomDecoder*: Decoder state - * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of - * the stream (must be the same characteristics as used for the - * encoder) - * @param [in] channels int: Number of channels - * @return OPUS_OK Success or @ref opus_errorcodes - */ -OPUS_CUSTOM_EXPORT_STATIC int opus_custom_decoder_init( - OpusCustomDecoder *st, - const OpusCustomMode *mode, - int channels -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); - -#endif - - -/** Creates a new decoder state. Each stream needs its own decoder state (can't - * be shared across simultaneous streams). - * @param [in] mode OpusCustomMode: Contains all the information about the characteristics of the - * stream (must be the same characteristics as used for the encoder) - * @param [in] channels int: Number of channels - * @param [out] error int*: Returns an error code - * @return Newly created decoder state. - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomDecoder *opus_custom_decoder_create( - const OpusCustomMode *mode, - int channels, - int *error -) OPUS_ARG_NONNULL(1); - -/** Destroys a an decoder state. - * @param[in] st OpusCustomDecoder*: State to be freed. - */ -OPUS_CUSTOM_EXPORT void opus_custom_decoder_destroy(OpusCustomDecoder *st); - -/** Decode an opus custom frame with floating point output - * @param [in] st OpusCustomDecoder*: Decoder state - * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss - * @param [in] len int: Number of bytes in payload - * @param [out] pcm float*: Output signal (interleaved if 2 channels). length - * is frame_size*channels*sizeof(float) - * @param [in] frame_size Number of samples per channel of available space in *pcm. - * @returns Number of decoded samples or @ref opus_errorcodes - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode_float( - OpusCustomDecoder *st, - const unsigned char *data, - int len, - float *pcm, - int frame_size -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Decode an opus custom frame - * @param [in] st OpusCustomDecoder*: Decoder state - * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss - * @param [in] len int: Number of bytes in payload - * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length - * is frame_size*channels*sizeof(opus_int16) - * @param [in] frame_size Number of samples per channel of available space in *pcm. - * @returns Number of decoded samples or @ref opus_errorcodes - */ -OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode( - OpusCustomDecoder *st, - const unsigned char *data, - int len, - opus_int16 *pcm, - int frame_size -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Perform a CTL function on an Opus custom decoder. - * - * Generally the request and subsequent arguments are generated - * by a convenience macro. - * @see opus_genericctls - */ -OPUS_CUSTOM_EXPORT int opus_custom_decoder_ctl(OpusCustomDecoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); - -/**@}*/ - -#ifdef __cplusplus -} -#endif - -#endif /* OPUS_CUSTOM_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_defines.h b/library/opusencoder/src/main/cpp/opus/include/opus_defines.h deleted file mode 100644 index fbf5d0eb74..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus_defines.h +++ /dev/null @@ -1,788 +0,0 @@ -/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited - Written by Jean-Marc Valin and Koen Vos */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - * @file opus_defines.h - * @brief Opus reference implementation constants - */ - -#ifndef OPUS_DEFINES_H -#define OPUS_DEFINES_H - -#include "opus_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** @defgroup opus_errorcodes Error codes - * @{ - */ -/** No error @hideinitializer*/ -#define OPUS_OK 0 -/** One or more invalid/out of range arguments @hideinitializer*/ -#define OPUS_BAD_ARG -1 -/** Not enough bytes allocated in the buffer @hideinitializer*/ -#define OPUS_BUFFER_TOO_SMALL -2 -/** An internal error was detected @hideinitializer*/ -#define OPUS_INTERNAL_ERROR -3 -/** The compressed data passed is corrupted @hideinitializer*/ -#define OPUS_INVALID_PACKET -4 -/** Invalid/unsupported request number @hideinitializer*/ -#define OPUS_UNIMPLEMENTED -5 -/** An encoder or decoder structure is invalid or already freed @hideinitializer*/ -#define OPUS_INVALID_STATE -6 -/** Memory allocation has failed @hideinitializer*/ -#define OPUS_ALLOC_FAIL -7 -/**@}*/ - -/** @cond OPUS_INTERNAL_DOC */ -/**Export control for opus functions */ - -#ifndef OPUS_EXPORT -# if defined(WIN32) -# if defined(OPUS_BUILD) && defined(DLL_EXPORT) -# define OPUS_EXPORT __declspec(dllexport) -# else -# define OPUS_EXPORT -# endif -# elif defined(__GNUC__) && defined(OPUS_BUILD) -# define OPUS_EXPORT __attribute__ ((visibility ("default"))) -# else -# define OPUS_EXPORT -# endif -#endif - -# if !defined(OPUS_GNUC_PREREQ) -# if defined(__GNUC__)&&defined(__GNUC_MINOR__) -# define OPUS_GNUC_PREREQ(_maj,_min) \ - ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) -# else -# define OPUS_GNUC_PREREQ(_maj,_min) 0 -# endif -# endif - -#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) -# if OPUS_GNUC_PREREQ(3,0) -# define OPUS_RESTRICT __restrict__ -# elif (defined(_MSC_VER) && _MSC_VER >= 1400) -# define OPUS_RESTRICT __restrict -# else -# define OPUS_RESTRICT -# endif -#else -# define OPUS_RESTRICT restrict -#endif - -#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) -# if OPUS_GNUC_PREREQ(2,7) -# define OPUS_INLINE __inline__ -# elif (defined(_MSC_VER)) -# define OPUS_INLINE __inline -# else -# define OPUS_INLINE -# endif -#else -# define OPUS_INLINE inline -#endif - -/**Warning attributes for opus functions - * NONNULL is not used in OPUS_BUILD to avoid the compiler optimizing out - * some paranoid null checks. */ -#if defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) -# define OPUS_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) -#else -# define OPUS_WARN_UNUSED_RESULT -#endif -#if !defined(OPUS_BUILD) && defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) -# define OPUS_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) -#else -# define OPUS_ARG_NONNULL(_x) -#endif - -/** These are the actual Encoder CTL ID numbers. - * They should not be used directly by applications. - * In general, SETs should be even and GETs should be odd.*/ -#define OPUS_SET_APPLICATION_REQUEST 4000 -#define OPUS_GET_APPLICATION_REQUEST 4001 -#define OPUS_SET_BITRATE_REQUEST 4002 -#define OPUS_GET_BITRATE_REQUEST 4003 -#define OPUS_SET_MAX_BANDWIDTH_REQUEST 4004 -#define OPUS_GET_MAX_BANDWIDTH_REQUEST 4005 -#define OPUS_SET_VBR_REQUEST 4006 -#define OPUS_GET_VBR_REQUEST 4007 -#define OPUS_SET_BANDWIDTH_REQUEST 4008 -#define OPUS_GET_BANDWIDTH_REQUEST 4009 -#define OPUS_SET_COMPLEXITY_REQUEST 4010 -#define OPUS_GET_COMPLEXITY_REQUEST 4011 -#define OPUS_SET_INBAND_FEC_REQUEST 4012 -#define OPUS_GET_INBAND_FEC_REQUEST 4013 -#define OPUS_SET_PACKET_LOSS_PERC_REQUEST 4014 -#define OPUS_GET_PACKET_LOSS_PERC_REQUEST 4015 -#define OPUS_SET_DTX_REQUEST 4016 -#define OPUS_GET_DTX_REQUEST 4017 -#define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020 -#define OPUS_GET_VBR_CONSTRAINT_REQUEST 4021 -#define OPUS_SET_FORCE_CHANNELS_REQUEST 4022 -#define OPUS_GET_FORCE_CHANNELS_REQUEST 4023 -#define OPUS_SET_SIGNAL_REQUEST 4024 -#define OPUS_GET_SIGNAL_REQUEST 4025 -#define OPUS_GET_LOOKAHEAD_REQUEST 4027 -/* #define OPUS_RESET_STATE 4028 */ -#define OPUS_GET_SAMPLE_RATE_REQUEST 4029 -#define OPUS_GET_FINAL_RANGE_REQUEST 4031 -#define OPUS_GET_PITCH_REQUEST 4033 -#define OPUS_SET_GAIN_REQUEST 4034 -#define OPUS_GET_GAIN_REQUEST 4045 /* Should have been 4035 */ -#define OPUS_SET_LSB_DEPTH_REQUEST 4036 -#define OPUS_GET_LSB_DEPTH_REQUEST 4037 -#define OPUS_GET_LAST_PACKET_DURATION_REQUEST 4039 -#define OPUS_SET_EXPERT_FRAME_DURATION_REQUEST 4040 -#define OPUS_GET_EXPERT_FRAME_DURATION_REQUEST 4041 -#define OPUS_SET_PREDICTION_DISABLED_REQUEST 4042 -#define OPUS_GET_PREDICTION_DISABLED_REQUEST 4043 -/* Don't use 4045, it's already taken by OPUS_GET_GAIN_REQUEST */ -#define OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST 4046 -#define OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST 4047 - -/** Defines for the presence of extended APIs. */ -#define OPUS_HAVE_OPUS_PROJECTION_H - -/* Macros to trigger compilation errors when the wrong types are provided to a CTL */ -#define __opus_check_int(x) (((void)((x) == (opus_int32)0)), (opus_int32)(x)) -#define __opus_check_int_ptr(ptr) ((ptr) + ((ptr) - (opus_int32*)(ptr))) -#define __opus_check_uint_ptr(ptr) ((ptr) + ((ptr) - (opus_uint32*)(ptr))) -#define __opus_check_val16_ptr(ptr) ((ptr) + ((ptr) - (opus_val16*)(ptr))) -/** @endcond */ - -/** @defgroup opus_ctlvalues Pre-defined values for CTL interface - * @see opus_genericctls, opus_encoderctls - * @{ - */ -/* Values for the various encoder CTLs */ -#define OPUS_AUTO -1000 /**opus_int32: Allowed values: 0-10, inclusive. - * - * @hideinitializer */ -#define OPUS_SET_COMPLEXITY(x) OPUS_SET_COMPLEXITY_REQUEST, __opus_check_int(x) -/** Gets the encoder's complexity configuration. - * @see OPUS_SET_COMPLEXITY - * @param[out] x opus_int32 *: Returns a value in the range 0-10, - * inclusive. - * @hideinitializer */ -#define OPUS_GET_COMPLEXITY(x) OPUS_GET_COMPLEXITY_REQUEST, __opus_check_int_ptr(x) - -/** Configures the bitrate in the encoder. - * Rates from 500 to 512000 bits per second are meaningful, as well as the - * special values #OPUS_AUTO and #OPUS_BITRATE_MAX. - * The value #OPUS_BITRATE_MAX can be used to cause the codec to use as much - * rate as it can, which is useful for controlling the rate by adjusting the - * output buffer size. - * @see OPUS_GET_BITRATE - * @param[in] x opus_int32: Bitrate in bits per second. The default - * is determined based on the number of - * channels and the input sampling rate. - * @hideinitializer */ -#define OPUS_SET_BITRATE(x) OPUS_SET_BITRATE_REQUEST, __opus_check_int(x) -/** Gets the encoder's bitrate configuration. - * @see OPUS_SET_BITRATE - * @param[out] x opus_int32 *: Returns the bitrate in bits per second. - * The default is determined based on the - * number of channels and the input - * sampling rate. - * @hideinitializer */ -#define OPUS_GET_BITRATE(x) OPUS_GET_BITRATE_REQUEST, __opus_check_int_ptr(x) - -/** Enables or disables variable bitrate (VBR) in the encoder. - * The configured bitrate may not be met exactly because frames must - * be an integer number of bytes in length. - * @see OPUS_GET_VBR - * @see OPUS_SET_VBR_CONSTRAINT - * @param[in] x opus_int32: Allowed values: - *
- *
0
Hard CBR. For LPC/hybrid modes at very low bit-rate, this can - * cause noticeable quality degradation.
- *
1
VBR (default). The exact type of VBR is controlled by - * #OPUS_SET_VBR_CONSTRAINT.
- *
- * @hideinitializer */ -#define OPUS_SET_VBR(x) OPUS_SET_VBR_REQUEST, __opus_check_int(x) -/** Determine if variable bitrate (VBR) is enabled in the encoder. - * @see OPUS_SET_VBR - * @see OPUS_GET_VBR_CONSTRAINT - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
Hard CBR.
- *
1
VBR (default). The exact type of VBR may be retrieved via - * #OPUS_GET_VBR_CONSTRAINT.
- *
- * @hideinitializer */ -#define OPUS_GET_VBR(x) OPUS_GET_VBR_REQUEST, __opus_check_int_ptr(x) - -/** Enables or disables constrained VBR in the encoder. - * This setting is ignored when the encoder is in CBR mode. - * @warning Only the MDCT mode of Opus currently heeds the constraint. - * Speech mode ignores it completely, hybrid mode may fail to obey it - * if the LPC layer uses more bitrate than the constraint would have - * permitted. - * @see OPUS_GET_VBR_CONSTRAINT - * @see OPUS_SET_VBR - * @param[in] x opus_int32: Allowed values: - *
- *
0
Unconstrained VBR.
- *
1
Constrained VBR (default). This creates a maximum of one - * frame of buffering delay assuming a transport with a - * serialization speed of the nominal bitrate.
- *
- * @hideinitializer */ -#define OPUS_SET_VBR_CONSTRAINT(x) OPUS_SET_VBR_CONSTRAINT_REQUEST, __opus_check_int(x) -/** Determine if constrained VBR is enabled in the encoder. - * @see OPUS_SET_VBR_CONSTRAINT - * @see OPUS_GET_VBR - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
Unconstrained VBR.
- *
1
Constrained VBR (default).
- *
- * @hideinitializer */ -#define OPUS_GET_VBR_CONSTRAINT(x) OPUS_GET_VBR_CONSTRAINT_REQUEST, __opus_check_int_ptr(x) - -/** Configures mono/stereo forcing in the encoder. - * This can force the encoder to produce packets encoded as either mono or - * stereo, regardless of the format of the input audio. This is useful when - * the caller knows that the input signal is currently a mono source embedded - * in a stereo stream. - * @see OPUS_GET_FORCE_CHANNELS - * @param[in] x opus_int32: Allowed values: - *
- *
#OPUS_AUTO
Not forced (default)
- *
1
Forced mono
- *
2
Forced stereo
- *
- * @hideinitializer */ -#define OPUS_SET_FORCE_CHANNELS(x) OPUS_SET_FORCE_CHANNELS_REQUEST, __opus_check_int(x) -/** Gets the encoder's forced channel configuration. - * @see OPUS_SET_FORCE_CHANNELS - * @param[out] x opus_int32 *: - *
- *
#OPUS_AUTO
Not forced (default)
- *
1
Forced mono
- *
2
Forced stereo
- *
- * @hideinitializer */ -#define OPUS_GET_FORCE_CHANNELS(x) OPUS_GET_FORCE_CHANNELS_REQUEST, __opus_check_int_ptr(x) - -/** Configures the maximum bandpass that the encoder will select automatically. - * Applications should normally use this instead of #OPUS_SET_BANDWIDTH - * (leaving that set to the default, #OPUS_AUTO). This allows the - * application to set an upper bound based on the type of input it is - * providing, but still gives the encoder the freedom to reduce the bandpass - * when the bitrate becomes too low, for better overall quality. - * @see OPUS_GET_MAX_BANDWIDTH - * @param[in] x opus_int32: Allowed values: - *
- *
OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
- *
OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
- *
OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
- *
OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
- *
OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
- *
- * @hideinitializer */ -#define OPUS_SET_MAX_BANDWIDTH(x) OPUS_SET_MAX_BANDWIDTH_REQUEST, __opus_check_int(x) - -/** Gets the encoder's configured maximum allowed bandpass. - * @see OPUS_SET_MAX_BANDWIDTH - * @param[out] x opus_int32 *: Allowed values: - *
- *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
- *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
- *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
- *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
- *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband (default)
- *
- * @hideinitializer */ -#define OPUS_GET_MAX_BANDWIDTH(x) OPUS_GET_MAX_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) - -/** Sets the encoder's bandpass to a specific value. - * This prevents the encoder from automatically selecting the bandpass based - * on the available bitrate. If an application knows the bandpass of the input - * audio it is providing, it should normally use #OPUS_SET_MAX_BANDWIDTH - * instead, which still gives the encoder the freedom to reduce the bandpass - * when the bitrate becomes too low, for better overall quality. - * @see OPUS_GET_BANDWIDTH - * @param[in] x opus_int32: Allowed values: - *
- *
#OPUS_AUTO
(default)
- *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
- *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
- *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
- *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
- *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
- *
- * @hideinitializer */ -#define OPUS_SET_BANDWIDTH(x) OPUS_SET_BANDWIDTH_REQUEST, __opus_check_int(x) - -/** Configures the type of signal being encoded. - * This is a hint which helps the encoder's mode selection. - * @see OPUS_GET_SIGNAL - * @param[in] x opus_int32: Allowed values: - *
- *
#OPUS_AUTO
(default)
- *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
- *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
- *
- * @hideinitializer */ -#define OPUS_SET_SIGNAL(x) OPUS_SET_SIGNAL_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured signal type. - * @see OPUS_SET_SIGNAL - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
#OPUS_AUTO
(default)
- *
#OPUS_SIGNAL_VOICE
Bias thresholds towards choosing LPC or Hybrid modes.
- *
#OPUS_SIGNAL_MUSIC
Bias thresholds towards choosing MDCT modes.
- *
- * @hideinitializer */ -#define OPUS_GET_SIGNAL(x) OPUS_GET_SIGNAL_REQUEST, __opus_check_int_ptr(x) - - -/** Configures the encoder's intended application. - * The initial value is a mandatory argument to the encoder_create function. - * @see OPUS_GET_APPLICATION - * @param[in] x opus_int32: Returns one of the following values: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @hideinitializer */ -#define OPUS_SET_APPLICATION(x) OPUS_SET_APPLICATION_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured application. - * @see OPUS_SET_APPLICATION - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @hideinitializer */ -#define OPUS_GET_APPLICATION(x) OPUS_GET_APPLICATION_REQUEST, __opus_check_int_ptr(x) - -/** Gets the total samples of delay added by the entire codec. - * This can be queried by the encoder and then the provided number of samples can be - * skipped on from the start of the decoder's output to provide time aligned input - * and output. From the perspective of a decoding application the real data begins this many - * samples late. - * - * The decoder contribution to this delay is identical for all decoders, but the - * encoder portion of the delay may vary from implementation to implementation, - * version to version, or even depend on the encoder's initial configuration. - * Applications needing delay compensation should call this CTL rather than - * hard-coding a value. - * @param[out] x opus_int32 *: Number of lookahead samples - * @hideinitializer */ -#define OPUS_GET_LOOKAHEAD(x) OPUS_GET_LOOKAHEAD_REQUEST, __opus_check_int_ptr(x) - -/** Configures the encoder's use of inband forward error correction (FEC). - * @note This is only applicable to the LPC layer - * @see OPUS_GET_INBAND_FEC - * @param[in] x opus_int32: Allowed values: - *
- *
0
Disable inband FEC (default).
- *
1
Enable inband FEC.
- *
- * @hideinitializer */ -#define OPUS_SET_INBAND_FEC(x) OPUS_SET_INBAND_FEC_REQUEST, __opus_check_int(x) -/** Gets encoder's configured use of inband forward error correction. - * @see OPUS_SET_INBAND_FEC - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
Inband FEC disabled (default).
- *
1
Inband FEC enabled.
- *
- * @hideinitializer */ -#define OPUS_GET_INBAND_FEC(x) OPUS_GET_INBAND_FEC_REQUEST, __opus_check_int_ptr(x) - -/** Configures the encoder's expected packet loss percentage. - * Higher values trigger progressively more loss resistant behavior in the encoder - * at the expense of quality at a given bitrate in the absence of packet loss, but - * greater quality under loss. - * @see OPUS_GET_PACKET_LOSS_PERC - * @param[in] x opus_int32: Loss percentage in the range 0-100, inclusive (default: 0). - * @hideinitializer */ -#define OPUS_SET_PACKET_LOSS_PERC(x) OPUS_SET_PACKET_LOSS_PERC_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured packet loss percentage. - * @see OPUS_SET_PACKET_LOSS_PERC - * @param[out] x opus_int32 *: Returns the configured loss percentage - * in the range 0-100, inclusive (default: 0). - * @hideinitializer */ -#define OPUS_GET_PACKET_LOSS_PERC(x) OPUS_GET_PACKET_LOSS_PERC_REQUEST, __opus_check_int_ptr(x) - -/** Configures the encoder's use of discontinuous transmission (DTX). - * @note This is only applicable to the LPC layer - * @see OPUS_GET_DTX - * @param[in] x opus_int32: Allowed values: - *
- *
0
Disable DTX (default).
- *
1
Enabled DTX.
- *
- * @hideinitializer */ -#define OPUS_SET_DTX(x) OPUS_SET_DTX_REQUEST, __opus_check_int(x) -/** Gets encoder's configured use of discontinuous transmission. - * @see OPUS_SET_DTX - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
DTX disabled (default).
- *
1
DTX enabled.
- *
- * @hideinitializer */ -#define OPUS_GET_DTX(x) OPUS_GET_DTX_REQUEST, __opus_check_int_ptr(x) -/** Configures the depth of signal being encoded. - * - * This is a hint which helps the encoder identify silence and near-silence. - * It represents the number of significant bits of linear intensity below - * which the signal contains ignorable quantization or other noise. - * - * For example, OPUS_SET_LSB_DEPTH(14) would be an appropriate setting - * for G.711 u-law input. OPUS_SET_LSB_DEPTH(16) would be appropriate - * for 16-bit linear pcm input with opus_encode_float(). - * - * When using opus_encode() instead of opus_encode_float(), or when libopus - * is compiled for fixed-point, the encoder uses the minimum of the value - * set here and the value 16. - * - * @see OPUS_GET_LSB_DEPTH - * @param[in] x opus_int32: Input precision in bits, between 8 and 24 - * (default: 24). - * @hideinitializer */ -#define OPUS_SET_LSB_DEPTH(x) OPUS_SET_LSB_DEPTH_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured signal depth. - * @see OPUS_SET_LSB_DEPTH - * @param[out] x opus_int32 *: Input precision in bits, between 8 and - * 24 (default: 24). - * @hideinitializer */ -#define OPUS_GET_LSB_DEPTH(x) OPUS_GET_LSB_DEPTH_REQUEST, __opus_check_int_ptr(x) - -/** Configures the encoder's use of variable duration frames. - * When variable duration is enabled, the encoder is free to use a shorter frame - * size than the one requested in the opus_encode*() call. - * It is then the user's responsibility - * to verify how much audio was encoded by checking the ToC byte of the encoded - * packet. The part of the audio that was not encoded needs to be resent to the - * encoder for the next call. Do not use this option unless you really - * know what you are doing. - * @see OPUS_GET_EXPERT_FRAME_DURATION - * @param[in] x opus_int32: Allowed values: - *
- *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
- *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
- *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
- *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
- *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
- *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
- *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
- *
OPUS_FRAMESIZE_80_MS
Use 80 ms frames.
- *
OPUS_FRAMESIZE_100_MS
Use 100 ms frames.
- *
OPUS_FRAMESIZE_120_MS
Use 120 ms frames.
- *
- * @hideinitializer */ -#define OPUS_SET_EXPERT_FRAME_DURATION(x) OPUS_SET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured use of variable duration frames. - * @see OPUS_SET_EXPERT_FRAME_DURATION - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
OPUS_FRAMESIZE_ARG
Select frame size from the argument (default).
- *
OPUS_FRAMESIZE_2_5_MS
Use 2.5 ms frames.
- *
OPUS_FRAMESIZE_5_MS
Use 5 ms frames.
- *
OPUS_FRAMESIZE_10_MS
Use 10 ms frames.
- *
OPUS_FRAMESIZE_20_MS
Use 20 ms frames.
- *
OPUS_FRAMESIZE_40_MS
Use 40 ms frames.
- *
OPUS_FRAMESIZE_60_MS
Use 60 ms frames.
- *
OPUS_FRAMESIZE_80_MS
Use 80 ms frames.
- *
OPUS_FRAMESIZE_100_MS
Use 100 ms frames.
- *
OPUS_FRAMESIZE_120_MS
Use 120 ms frames.
- *
- * @hideinitializer */ -#define OPUS_GET_EXPERT_FRAME_DURATION(x) OPUS_GET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int_ptr(x) - -/** If set to 1, disables almost all use of prediction, making frames almost - * completely independent. This reduces quality. - * @see OPUS_GET_PREDICTION_DISABLED - * @param[in] x opus_int32: Allowed values: - *
- *
0
Enable prediction (default).
- *
1
Disable prediction.
- *
- * @hideinitializer */ -#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured prediction status. - * @see OPUS_SET_PREDICTION_DISABLED - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
Prediction enabled (default).
- *
1
Prediction disabled.
- *
- * @hideinitializer */ -#define OPUS_GET_PREDICTION_DISABLED(x) OPUS_GET_PREDICTION_DISABLED_REQUEST, __opus_check_int_ptr(x) - -/**@}*/ - -/** @defgroup opus_genericctls Generic CTLs - * - * These macros are used with the \c opus_decoder_ctl and - * \c opus_encoder_ctl calls to generate a particular - * request. - * - * When called on an \c OpusDecoder they apply to that - * particular decoder instance. When called on an - * \c OpusEncoder they apply to the corresponding setting - * on that encoder instance, if present. - * - * Some usage examples: - * - * @code - * int ret; - * opus_int32 pitch; - * ret = opus_decoder_ctl(dec_ctx, OPUS_GET_PITCH(&pitch)); - * if (ret == OPUS_OK) return ret; - * - * opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE); - * opus_decoder_ctl(dec_ctx, OPUS_RESET_STATE); - * - * opus_int32 enc_bw, dec_bw; - * opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&enc_bw)); - * opus_decoder_ctl(dec_ctx, OPUS_GET_BANDWIDTH(&dec_bw)); - * if (enc_bw != dec_bw) { - * printf("packet bandwidth mismatch!\n"); - * } - * @endcode - * - * @see opus_encoder, opus_decoder_ctl, opus_encoder_ctl, opus_decoderctls, opus_encoderctls - * @{ - */ - -/** Resets the codec state to be equivalent to a freshly initialized state. - * This should be called when switching streams in order to prevent - * the back to back decoding from giving different results from - * one at a time decoding. - * @hideinitializer */ -#define OPUS_RESET_STATE 4028 - -/** Gets the final state of the codec's entropy coder. - * This is used for testing purposes, - * The encoder and decoder state should be identical after coding a payload - * (assuming no data corruption or software bugs) - * - * @param[out] x opus_uint32 *: Entropy coder state - * - * @hideinitializer */ -#define OPUS_GET_FINAL_RANGE(x) OPUS_GET_FINAL_RANGE_REQUEST, __opus_check_uint_ptr(x) - -/** Gets the encoder's configured bandpass or the decoder's last bandpass. - * @see OPUS_SET_BANDWIDTH - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
#OPUS_AUTO
(default)
- *
#OPUS_BANDWIDTH_NARROWBAND
4 kHz passband
- *
#OPUS_BANDWIDTH_MEDIUMBAND
6 kHz passband
- *
#OPUS_BANDWIDTH_WIDEBAND
8 kHz passband
- *
#OPUS_BANDWIDTH_SUPERWIDEBAND
12 kHz passband
- *
#OPUS_BANDWIDTH_FULLBAND
20 kHz passband
- *
- * @hideinitializer */ -#define OPUS_GET_BANDWIDTH(x) OPUS_GET_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) - -/** Gets the sampling rate the encoder or decoder was initialized with. - * This simply returns the Fs value passed to opus_encoder_init() - * or opus_decoder_init(). - * @param[out] x opus_int32 *: Sampling rate of encoder or decoder. - * @hideinitializer - */ -#define OPUS_GET_SAMPLE_RATE(x) OPUS_GET_SAMPLE_RATE_REQUEST, __opus_check_int_ptr(x) - -/** If set to 1, disables the use of phase inversion for intensity stereo, - * improving the quality of mono downmixes, but slightly reducing normal - * stereo quality. Disabling phase inversion in the decoder does not comply - * with RFC 6716, although it does not cause any interoperability issue and - * is expected to become part of the Opus standard once RFC 6716 is updated - * by draft-ietf-codec-opus-update. - * @see OPUS_GET_PHASE_INVERSION_DISABLED - * @param[in] x opus_int32: Allowed values: - *
- *
0
Enable phase inversion (default).
- *
1
Disable phase inversion.
- *
- * @hideinitializer */ -#define OPUS_SET_PHASE_INVERSION_DISABLED(x) OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int(x) -/** Gets the encoder's configured phase inversion status. - * @see OPUS_SET_PHASE_INVERSION_DISABLED - * @param[out] x opus_int32 *: Returns one of the following values: - *
- *
0
Stereo phase inversion enabled (default).
- *
1
Stereo phase inversion disabled.
- *
- * @hideinitializer */ -#define OPUS_GET_PHASE_INVERSION_DISABLED(x) OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int_ptr(x) - -/**@}*/ - -/** @defgroup opus_decoderctls Decoder related CTLs - * @see opus_genericctls, opus_encoderctls, opus_decoder - * @{ - */ - -/** Configures decoder gain adjustment. - * Scales the decoded output by a factor specified in Q8 dB units. - * This has a maximum range of -32768 to 32767 inclusive, and returns - * OPUS_BAD_ARG otherwise. The default is zero indicating no adjustment. - * This setting survives decoder reset. - * - * gain = pow(10, x/(20.0*256)) - * - * @param[in] x opus_int32: Amount to scale PCM signal by in Q8 dB units. - * @hideinitializer */ -#define OPUS_SET_GAIN(x) OPUS_SET_GAIN_REQUEST, __opus_check_int(x) -/** Gets the decoder's configured gain adjustment. @see OPUS_SET_GAIN - * - * @param[out] x opus_int32 *: Amount to scale PCM signal by in Q8 dB units. - * @hideinitializer */ -#define OPUS_GET_GAIN(x) OPUS_GET_GAIN_REQUEST, __opus_check_int_ptr(x) - -/** Gets the duration (in samples) of the last packet successfully decoded or concealed. - * @param[out] x opus_int32 *: Number of samples (at current sampling rate). - * @hideinitializer */ -#define OPUS_GET_LAST_PACKET_DURATION(x) OPUS_GET_LAST_PACKET_DURATION_REQUEST, __opus_check_int_ptr(x) - -/** Gets the pitch of the last decoded frame, if available. - * This can be used for any post-processing algorithm requiring the use of pitch, - * e.g. time stretching/shortening. If the last frame was not voiced, or if the - * pitch was not coded in the frame, then zero is returned. - * - * This CTL is only implemented for decoder instances. - * - * @param[out] x opus_int32 *: pitch period at 48 kHz (or 0 if not available) - * - * @hideinitializer */ -#define OPUS_GET_PITCH(x) OPUS_GET_PITCH_REQUEST, __opus_check_int_ptr(x) - -/**@}*/ - -/** @defgroup opus_libinfo Opus library information functions - * @{ - */ - -/** Converts an opus error code into a human readable string. - * - * @param[in] error int: Error number - * @returns Error string - */ -OPUS_EXPORT const char *opus_strerror(int error); - -/** Gets the libopus version string. - * - * Applications may look for the substring "-fixed" in the version string to - * determine whether they have a fixed-point or floating-point build at - * runtime. - * - * @returns Version string - */ -OPUS_EXPORT const char *opus_get_version_string(void); -/**@}*/ - -#ifdef __cplusplus -} -#endif - -#endif /* OPUS_DEFINES_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h b/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h deleted file mode 100644 index babcee6905..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus_multistream.h +++ /dev/null @@ -1,660 +0,0 @@ -/* Copyright (c) 2011 Xiph.Org Foundation - Written by Jean-Marc Valin */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - * @file opus_multistream.h - * @brief Opus reference implementation multistream API - */ - -#ifndef OPUS_MULTISTREAM_H -#define OPUS_MULTISTREAM_H - -#include "opus.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** @cond OPUS_INTERNAL_DOC */ - -/** Macros to trigger compilation errors when the wrong types are provided to a - * CTL. */ -/**@{*/ -#define __opus_check_encstate_ptr(ptr) ((ptr) + ((ptr) - (OpusEncoder**)(ptr))) -#define __opus_check_decstate_ptr(ptr) ((ptr) + ((ptr) - (OpusDecoder**)(ptr))) -/**@}*/ - -/** These are the actual encoder and decoder CTL ID numbers. - * They should not be used directly by applications. - * In general, SETs should be even and GETs should be odd.*/ -/**@{*/ -#define OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST 5120 -#define OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST 5122 -/**@}*/ - -/** @endcond */ - -/** @defgroup opus_multistream_ctls Multistream specific encoder and decoder CTLs - * - * These are convenience macros that are specific to the - * opus_multistream_encoder_ctl() and opus_multistream_decoder_ctl() - * interface. - * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, and - * @ref opus_decoderctls may be applied to a multistream encoder or decoder as - * well. - * In addition, you may retrieve the encoder or decoder state for an specific - * stream via #OPUS_MULTISTREAM_GET_ENCODER_STATE or - * #OPUS_MULTISTREAM_GET_DECODER_STATE and apply CTLs to it individually. - */ -/**@{*/ - -/** Gets the encoder state for an individual stream of a multistream encoder. - * @param[in] x opus_int32: The index of the stream whose encoder you - * wish to retrieve. - * This must be non-negative and less than - * the streams parameter used - * to initialize the encoder. - * @param[out] y OpusEncoder**: Returns a pointer to the given - * encoder state. - * @retval OPUS_BAD_ARG The index of the requested stream was out of range. - * @hideinitializer - */ -#define OPUS_MULTISTREAM_GET_ENCODER_STATE(x,y) OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, __opus_check_int(x), __opus_check_encstate_ptr(y) - -/** Gets the decoder state for an individual stream of a multistream decoder. - * @param[in] x opus_int32: The index of the stream whose decoder you - * wish to retrieve. - * This must be non-negative and less than - * the streams parameter used - * to initialize the decoder. - * @param[out] y OpusDecoder**: Returns a pointer to the given - * decoder state. - * @retval OPUS_BAD_ARG The index of the requested stream was out of range. - * @hideinitializer - */ -#define OPUS_MULTISTREAM_GET_DECODER_STATE(x,y) OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST, __opus_check_int(x), __opus_check_decstate_ptr(y) - -/**@}*/ - -/** @defgroup opus_multistream Opus Multistream API - * @{ - * - * The multistream API allows individual Opus streams to be combined into a - * single packet, enabling support for up to 255 channels. Unlike an - * elementary Opus stream, the encoder and decoder must negotiate the channel - * configuration before the decoder can successfully interpret the data in the - * packets produced by the encoder. Some basic information, such as packet - * duration, can be computed without any special negotiation. - * - * The format for multistream Opus packets is defined in - * RFC 7845 - * and is based on the self-delimited Opus framing described in Appendix B of - * RFC 6716. - * Normal Opus packets are just a degenerate case of multistream Opus packets, - * and can be encoded or decoded with the multistream API by setting - * streams to 1 when initializing the encoder or - * decoder. - * - * Multistream Opus streams can contain up to 255 elementary Opus streams. - * These may be either "uncoupled" or "coupled", indicating that the decoder - * is configured to decode them to either 1 or 2 channels, respectively. - * The streams are ordered so that all coupled streams appear at the - * beginning. - * - * A mapping table defines which decoded channel i - * should be used for each input/output (I/O) channel j. This table is - * typically provided as an unsigned char array. - * Let i = mapping[j] be the index for I/O channel j. - * If i < 2*coupled_streams, then I/O channel j is - * encoded as the left channel of stream (i/2) if i - * is even, or as the right channel of stream (i/2) if - * i is odd. Otherwise, I/O channel j is encoded as - * mono in stream (i - coupled_streams), unless it has the special - * value 255, in which case it is omitted from the encoding entirely (the - * decoder will reproduce it as silence). Each value i must either - * be the special value 255 or be less than streams + coupled_streams. - * - * The output channels specified by the encoder - * should use the - * Vorbis - * channel ordering. A decoder may wish to apply an additional permutation - * to the mapping the encoder used to achieve a different output channel - * order (e.g. for outputing in WAV order). - * - * Each multistream packet contains an Opus packet for each stream, and all of - * the Opus packets in a single multistream packet must have the same - * duration. Therefore the duration of a multistream packet can be extracted - * from the TOC sequence of the first stream, which is located at the - * beginning of the packet, just like an elementary Opus stream: - * - * @code - * int nb_samples; - * int nb_frames; - * nb_frames = opus_packet_get_nb_frames(data, len); - * if (nb_frames < 1) - * return nb_frames; - * nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames; - * @endcode - * - * The general encoding and decoding process proceeds exactly the same as in - * the normal @ref opus_encoder and @ref opus_decoder APIs. - * See their documentation for an overview of how to use the corresponding - * multistream functions. - */ - -/** Opus multistream encoder state. - * This contains the complete state of a multistream Opus encoder. - * It is position independent and can be freely copied. - * @see opus_multistream_encoder_create - * @see opus_multistream_encoder_init - */ -typedef struct OpusMSEncoder OpusMSEncoder; - -/** Opus multistream decoder state. - * This contains the complete state of a multistream Opus decoder. - * It is position independent and can be freely copied. - * @see opus_multistream_decoder_create - * @see opus_multistream_decoder_init - */ -typedef struct OpusMSDecoder OpusMSDecoder; - -/**\name Multistream encoder functions */ -/**@{*/ - -/** Gets the size of an OpusMSEncoder structure. - * @param streams int: The total number of streams to encode from the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number of coupled (2 channel) streams - * to encode. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * encoded channels (streams + - * coupled_streams) must be no - * more than 255. - * @returns The size in bytes on success, or a negative error code - * (see @ref opus_errorcodes) on error. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size( - int streams, - int coupled_streams -); - -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size( - int channels, - int mapping_family -); - - -/** Allocates and initializes a multistream encoder state. - * Call opus_multistream_encoder_destroy() to release - * this object when finished. - * @param Fs opus_int32: Sampling rate of the input signal (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels in the input signal. - * This must be at most 255. - * It may be greater than the number of - * coded channels (streams + - * coupled_streams). - * @param streams int: The total number of streams to encode from the - * input. - * This must be no more than the number of channels. - * @param coupled_streams int: Number of coupled (2 channel) streams - * to encode. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * encoded channels (streams + - * coupled_streams) must be no - * more than the number of input channels. - * @param[in] mapping const unsigned char[channels]: Mapping from - * encoded channels to input channels, as described in - * @ref opus_multistream. As an extra constraint, the - * multistream encoder does not allow encoding coupled - * streams for which one channel is unused since this - * is never a good idea. - * @param application int: The target encoder application. - * This must be one of the following: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @param[out] error int *: Returns #OPUS_OK on success, or an error - * code (see @ref opus_errorcodes) on - * failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create( - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - const unsigned char *mapping, - int application, - int *error -) OPUS_ARG_NONNULL(5); - -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create( - opus_int32 Fs, - int channels, - int mapping_family, - int *streams, - int *coupled_streams, - unsigned char *mapping, - int application, - int *error -) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); - -/** Initialize a previously allocated multistream encoder state. - * The memory pointed to by \a st must be at least the size returned by - * opus_multistream_encoder_get_size(). - * This is intended for applications which use their own allocator instead of - * malloc. - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @see opus_multistream_encoder_create - * @see opus_multistream_encoder_get_size - * @param st OpusMSEncoder*: Multistream encoder state to initialize. - * @param Fs opus_int32: Sampling rate of the input signal (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels in the input signal. - * This must be at most 255. - * It may be greater than the number of - * coded channels (streams + - * coupled_streams). - * @param streams int: The total number of streams to encode from the - * input. - * This must be no more than the number of channels. - * @param coupled_streams int: Number of coupled (2 channel) streams - * to encode. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * encoded channels (streams + - * coupled_streams) must be no - * more than the number of input channels. - * @param[in] mapping const unsigned char[channels]: Mapping from - * encoded channels to input channels, as described in - * @ref opus_multistream. As an extra constraint, the - * multistream encoder does not allow encoding coupled - * streams for which one channel is unused since this - * is never a good idea. - * @param application int: The target encoder application. - * This must be one of the following: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) - * on failure. - */ -OPUS_EXPORT int opus_multistream_encoder_init( - OpusMSEncoder *st, - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - const unsigned char *mapping, - int application -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); - -OPUS_EXPORT int opus_multistream_surround_encoder_init( - OpusMSEncoder *st, - opus_int32 Fs, - int channels, - int mapping_family, - int *streams, - int *coupled_streams, - unsigned char *mapping, - int application -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6) OPUS_ARG_NONNULL(7); - -/** Encodes a multistream Opus frame. - * @param st OpusMSEncoder*: Multistream encoder state. - * @param[in] pcm const opus_int16*: The input signal as interleaved - * samples. - * This must contain - * frame_size*channels - * samples. - * @param frame_size int: Number of samples per channel in the input - * signal. - * This must be an Opus frame size for the - * encoder's sampling rate. - * For example, at 48 kHz the permitted values - * are 120, 240, 480, 960, 1920, and 2880. - * Passing in a duration of less than 10 ms - * (480 samples at 48 kHz) will prevent the - * encoder from using the LPC or hybrid modes. - * @param[out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode( - OpusMSEncoder *st, - const opus_int16 *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Encodes a multistream Opus frame from floating point input. - * @param st OpusMSEncoder*: Multistream encoder state. - * @param[in] pcm const float*: The input signal as interleaved - * samples with a normal range of - * +/-1.0. - * Samples with a range beyond +/-1.0 - * are supported but will be clipped by - * decoders using the integer API and - * should only be used if it is known - * that the far end supports extended - * dynamic range. - * This must contain - * frame_size*channels - * samples. - * @param frame_size int: Number of samples per channel in the input - * signal. - * This must be an Opus frame size for the - * encoder's sampling rate. - * For example, at 48 kHz the permitted values - * are 120, 240, 480, 960, 1920, and 2880. - * Passing in a duration of less than 10 ms - * (480 samples at 48 kHz) will prevent the - * encoder from using the LPC or hybrid modes. - * @param[out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode_float( - OpusMSEncoder *st, - const float *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - -/** Frees an OpusMSEncoder allocated by - * opus_multistream_encoder_create(). - * @param st OpusMSEncoder*: Multistream encoder state to be freed. - */ -OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st); - -/** Perform a CTL function on a multistream Opus encoder. - * - * Generally the request and subsequent arguments are generated by a - * convenience macro. - * @param st OpusMSEncoder*: Multistream encoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls, - * @ref opus_encoderctls, or @ref opus_multistream_ctls. - * @see opus_genericctls - * @see opus_encoderctls - * @see opus_multistream_ctls - */ -OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); - -/**@}*/ - -/**\name Multistream decoder functions */ -/**@{*/ - -/** Gets the size of an OpusMSDecoder structure. - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @returns The size in bytes on success, or a negative error code - * (see @ref opus_errorcodes) on error. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size( - int streams, - int coupled_streams -); - -/** Allocates and initializes a multistream decoder state. - * Call opus_multistream_decoder_destroy() to release - * this object when finished. - * @param Fs opus_int32: Sampling rate to decode at (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels to output. - * This must be at most 255. - * It may be different from the number of coded - * channels (streams + - * coupled_streams). - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number of streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @param[in] mapping const unsigned char[channels]: Mapping from - * coded channels to output channels, as described in - * @ref opus_multistream. - * @param[out] error int *: Returns #OPUS_OK on success, or an error - * code (see @ref opus_errorcodes) on - * failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create( - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - const unsigned char *mapping, - int *error -) OPUS_ARG_NONNULL(5); - -/** Intialize a previously allocated decoder state object. - * The memory pointed to by \a st must be at least the size returned by - * opus_multistream_encoder_get_size(). - * This is intended for applications which use their own allocator instead of - * malloc. - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @see opus_multistream_decoder_create - * @see opus_multistream_deocder_get_size - * @param st OpusMSEncoder*: Multistream encoder state to initialize. - * @param Fs opus_int32: Sampling rate to decode at (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels to output. - * This must be at most 255. - * It may be different from the number of coded - * channels (streams + - * coupled_streams). - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number of streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @param[in] mapping const unsigned char[channels]: Mapping from - * coded channels to output channels, as described in - * @ref opus_multistream. - * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) - * on failure. - */ -OPUS_EXPORT int opus_multistream_decoder_init( - OpusMSDecoder *st, - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - const unsigned char *mapping -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); - -/** Decode a multistream Opus packet. - * @param st OpusMSDecoder*: Multistream decoder state. - * @param[in] data const unsigned char*: Input payload. - * Use a NULL - * pointer to indicate packet - * loss. - * @param len opus_int32: Number of bytes in payload. - * @param[out] pcm opus_int16*: Output signal, with interleaved - * samples. - * This must contain room for - * frame_size*channels - * samples. - * @param frame_size int: The number of samples per channel of - * available space in \a pcm. - * If this is less than the maximum packet duration - * (120 ms; 5760 for 48kHz), this function will not be capable - * of decoding some packets. In the case of PLC (data==NULL) - * or FEC (decode_fec=1), then frame_size needs to be exactly - * the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the - * next incoming packet. For the PLC and FEC cases, frame_size - * must be a multiple of 2.5 ms. - * @param decode_fec int: Flag (0 or 1) to request that any in-band - * forward error correction data be decoded. - * If no such data is available, the frame is - * decoded as if it were lost. - * @returns Number of samples decoded on success or a negative error code - * (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode( - OpusMSDecoder *st, - const unsigned char *data, - opus_int32 len, - opus_int16 *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Decode a multistream Opus packet with floating point output. - * @param st OpusMSDecoder*: Multistream decoder state. - * @param[in] data const unsigned char*: Input payload. - * Use a NULL - * pointer to indicate packet - * loss. - * @param len opus_int32: Number of bytes in payload. - * @param[out] pcm opus_int16*: Output signal, with interleaved - * samples. - * This must contain room for - * frame_size*channels - * samples. - * @param frame_size int: The number of samples per channel of - * available space in \a pcm. - * If this is less than the maximum packet duration - * (120 ms; 5760 for 48kHz), this function will not be capable - * of decoding some packets. In the case of PLC (data==NULL) - * or FEC (decode_fec=1), then frame_size needs to be exactly - * the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the - * next incoming packet. For the PLC and FEC cases, frame_size - * must be a multiple of 2.5 ms. - * @param decode_fec int: Flag (0 or 1) to request that any in-band - * forward error correction data be decoded. - * If no such data is available, the frame is - * decoded as if it were lost. - * @returns Number of samples decoded on success or a negative error code - * (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float( - OpusMSDecoder *st, - const unsigned char *data, - opus_int32 len, - float *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - -/** Perform a CTL function on a multistream Opus decoder. - * - * Generally the request and subsequent arguments are generated by a - * convenience macro. - * @param st OpusMSDecoder*: Multistream decoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls, - * @ref opus_decoderctls, or @ref opus_multistream_ctls. - * @see opus_genericctls - * @see opus_decoderctls - * @see opus_multistream_ctls - */ -OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); - -/** Frees an OpusMSDecoder allocated by - * opus_multistream_decoder_create(). - * @param st OpusMSDecoder: Multistream decoder state to be freed. - */ -OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st); - -/**@}*/ - -/**@}*/ - -#ifdef __cplusplus -} -#endif - -#endif /* OPUS_MULTISTREAM_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_projection.h b/library/opusencoder/src/main/cpp/opus/include/opus_projection.h deleted file mode 100644 index 9dabf4e85c..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus_projection.h +++ /dev/null @@ -1,568 +0,0 @@ -/* Copyright (c) 2017 Google Inc. - Written by Andrew Allen */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - * @file opus_projection.h - * @brief Opus projection reference API - */ - -#ifndef OPUS_PROJECTION_H -#define OPUS_PROJECTION_H - -#include "opus_multistream.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** @cond OPUS_INTERNAL_DOC */ - -/** These are the actual encoder and decoder CTL ID numbers. - * They should not be used directly by applications.c - * In general, SETs should be even and GETs should be odd.*/ -/**@{*/ -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST 6001 -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST 6003 -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST 6005 -/**@}*/ - - -/** @endcond */ - -/** @defgroup opus_projection_ctls Projection specific encoder and decoder CTLs - * - * These are convenience macros that are specific to the - * opus_projection_encoder_ctl() and opus_projection_decoder_ctl() - * interface. - * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, - * @ref opus_decoderctls, and @ref opus_multistream_ctls may be applied to a - * projection encoder or decoder as well. - */ -/**@{*/ - -/** Gets the gain (in dB. S7.8-format) of the demixing matrix from the encoder. - * @param[out] x opus_int32 *: Returns the gain (in dB. S7.8-format) - * of the demixing matrix. - * @hideinitializer - */ -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST, __opus_check_int_ptr(x) - - -/** Gets the size in bytes of the demixing matrix from the encoder. - * @param[out] x opus_int32 *: Returns the size in bytes of the - * demixing matrix. - * @hideinitializer - */ -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST, __opus_check_int_ptr(x) - - -/** Copies the demixing matrix to the supplied pointer location. - * @param[out] x unsigned char *: Returns the demixing matrix to the - * supplied pointer location. - * @param y opus_int32: The size in bytes of the reserved memory at the - * pointer location. - * @hideinitializer - */ -#define OPUS_PROJECTION_GET_DEMIXING_MATRIX(x,y) OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, x, __opus_check_int(y) - - -/**@}*/ - -/** Opus projection encoder state. - * This contains the complete state of a projection Opus encoder. - * It is position independent and can be freely copied. - * @see opus_projection_ambisonics_encoder_create - */ -typedef struct OpusProjectionEncoder OpusProjectionEncoder; - - -/** Opus projection decoder state. - * This contains the complete state of a projection Opus decoder. - * It is position independent and can be freely copied. - * @see opus_projection_decoder_create - * @see opus_projection_decoder_init - */ -typedef struct OpusProjectionDecoder OpusProjectionDecoder; - - -/**\name Projection encoder functions */ -/**@{*/ - -/** Gets the size of an OpusProjectionEncoder structure. - * @param channels int: The total number of input channels to encode. - * This must be no more than 255. - * @param mapping_family int: The mapping family to use for selecting - * the appropriate projection. - * @returns The size in bytes on success, or a negative error code - * (see @ref opus_errorcodes) on error. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_ambisonics_encoder_get_size( - int channels, - int mapping_family -); - - -/** Allocates and initializes a projection encoder state. - * Call opus_projection_encoder_destroy() to release - * this object when finished. - * @param Fs opus_int32: Sampling rate of the input signal (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels in the input signal. - * This must be at most 255. - * It may be greater than the number of - * coded channels (streams + - * coupled_streams). - * @param mapping_family int: The mapping family to use for selecting - * the appropriate projection. - * @param[out] streams int *: The total number of streams that will - * be encoded from the input. - * @param[out] coupled_streams int *: Number of coupled (2 channel) - * streams that will be encoded from the input. - * @param application int: The target encoder application. - * This must be one of the following: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @param[out] error int *: Returns #OPUS_OK on success, or an error - * code (see @ref opus_errorcodes) on - * failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionEncoder *opus_projection_ambisonics_encoder_create( - opus_int32 Fs, - int channels, - int mapping_family, - int *streams, - int *coupled_streams, - int application, - int *error -) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5); - - -/** Initialize a previously allocated projection encoder state. - * The memory pointed to by \a st must be at least the size returned by - * opus_projection_ambisonics_encoder_get_size(). - * This is intended for applications which use their own allocator instead of - * malloc. - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @see opus_projection_ambisonics_encoder_create - * @see opus_projection_ambisonics_encoder_get_size - * @param st OpusProjectionEncoder*: Projection encoder state to initialize. - * @param Fs opus_int32: Sampling rate of the input signal (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels in the input signal. - * This must be at most 255. - * It may be greater than the number of - * coded channels (streams + - * coupled_streams). - * @param streams int: The total number of streams to encode from the - * input. - * This must be no more than the number of channels. - * @param coupled_streams int: Number of coupled (2 channel) streams - * to encode. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * encoded channels (streams + - * coupled_streams) must be no - * more than the number of input channels. - * @param application int: The target encoder application. - * This must be one of the following: - *
- *
#OPUS_APPLICATION_VOIP
- *
Process signal for improved speech intelligibility.
- *
#OPUS_APPLICATION_AUDIO
- *
Favor faithfulness to the original input.
- *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
- *
Configure the minimum possible coding delay by disabling certain modes - * of operation.
- *
- * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) - * on failure. - */ -OPUS_EXPORT int opus_projection_ambisonics_encoder_init( - OpusProjectionEncoder *st, - opus_int32 Fs, - int channels, - int mapping_family, - int *streams, - int *coupled_streams, - int application -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); - - -/** Encodes a projection Opus frame. - * @param st OpusProjectionEncoder*: Projection encoder state. - * @param[in] pcm const opus_int16*: The input signal as interleaved - * samples. - * This must contain - * frame_size*channels - * samples. - * @param frame_size int: Number of samples per channel in the input - * signal. - * This must be an Opus frame size for the - * encoder's sampling rate. - * For example, at 48 kHz the permitted values - * are 120, 240, 480, 960, 1920, and 2880. - * Passing in a duration of less than 10 ms - * (480 samples at 48 kHz) will prevent the - * encoder from using the LPC or hybrid modes. - * @param[out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode( - OpusProjectionEncoder *st, - const opus_int16 *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - - -/** Encodes a projection Opus frame from floating point input. - * @param st OpusProjectionEncoder*: Projection encoder state. - * @param[in] pcm const float*: The input signal as interleaved - * samples with a normal range of - * +/-1.0. - * Samples with a range beyond +/-1.0 - * are supported but will be clipped by - * decoders using the integer API and - * should only be used if it is known - * that the far end supports extended - * dynamic range. - * This must contain - * frame_size*channels - * samples. - * @param frame_size int: Number of samples per channel in the input - * signal. - * This must be an Opus frame size for the - * encoder's sampling rate. - * For example, at 48 kHz the permitted values - * are 120, 240, 480, 960, 1920, and 2880. - * Passing in a duration of less than 10 ms - * (480 samples at 48 kHz) will prevent the - * encoder from using the LPC or hybrid modes. - * @param[out] data unsigned char*: Output payload. - * This must contain storage for at - * least \a max_data_bytes. - * @param [in] max_data_bytes opus_int32: Size of the allocated - * memory for the output - * payload. This may be - * used to impose an upper limit on - * the instant bitrate, but should - * not be used as the only bitrate - * control. Use #OPUS_SET_BITRATE to - * control the bitrate. - * @returns The length of the encoded packet (in bytes) on success or a - * negative error code (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode_float( - OpusProjectionEncoder *st, - const float *pcm, - int frame_size, - unsigned char *data, - opus_int32 max_data_bytes -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); - - -/** Frees an OpusProjectionEncoder allocated by - * opus_projection_ambisonics_encoder_create(). - * @param st OpusProjectionEncoder*: Projection encoder state to be freed. - */ -OPUS_EXPORT void opus_projection_encoder_destroy(OpusProjectionEncoder *st); - - -/** Perform a CTL function on a projection Opus encoder. - * - * Generally the request and subsequent arguments are generated by a - * convenience macro. - * @param st OpusProjectionEncoder*: Projection encoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls, - * @ref opus_encoderctls, @ref opus_multistream_ctls, or - * @ref opus_projection_ctls - * @see opus_genericctls - * @see opus_encoderctls - * @see opus_multistream_ctls - * @see opus_projection_ctls - */ -OPUS_EXPORT int opus_projection_encoder_ctl(OpusProjectionEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); - - -/**@}*/ - -/**\name Projection decoder functions */ -/**@{*/ - -/** Gets the size of an OpusProjectionDecoder structure. - * @param channels int: The total number of output channels. - * This must be no more than 255. - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @returns The size in bytes on success, or a negative error code - * (see @ref opus_errorcodes) on error. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_decoder_get_size( - int channels, - int streams, - int coupled_streams -); - - -/** Allocates and initializes a projection decoder state. - * Call opus_projection_decoder_destroy() to release - * this object when finished. - * @param Fs opus_int32: Sampling rate to decode at (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels to output. - * This must be at most 255. - * It may be different from the number of coded - * channels (streams + - * coupled_streams). - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number of streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix - * that mapping from coded channels to output channels, - * as described in @ref opus_projection and - * @ref opus_projection_ctls. - * @param demixing_matrix_size opus_int32: The size in bytes of the - * demixing matrix, as - * described in @ref - * opus_projection_ctls. - * @param[out] error int *: Returns #OPUS_OK on success, or an error - * code (see @ref opus_errorcodes) on - * failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionDecoder *opus_projection_decoder_create( - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - unsigned char *demixing_matrix, - opus_int32 demixing_matrix_size, - int *error -) OPUS_ARG_NONNULL(5); - - -/** Intialize a previously allocated projection decoder state object. - * The memory pointed to by \a st must be at least the size returned by - * opus_projection_decoder_get_size(). - * This is intended for applications which use their own allocator instead of - * malloc. - * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. - * @see opus_projection_decoder_create - * @see opus_projection_deocder_get_size - * @param st OpusProjectionDecoder*: Projection encoder state to initialize. - * @param Fs opus_int32: Sampling rate to decode at (in Hz). - * This must be one of 8000, 12000, 16000, - * 24000, or 48000. - * @param channels int: Number of channels to output. - * This must be at most 255. - * It may be different from the number of coded - * channels (streams + - * coupled_streams). - * @param streams int: The total number of streams coded in the - * input. - * This must be no more than 255. - * @param coupled_streams int: Number of streams to decode as coupled - * (2 channel) streams. - * This must be no larger than the total - * number of streams. - * Additionally, The total number of - * coded channels (streams + - * coupled_streams) must be no - * more than 255. - * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix - * that mapping from coded channels to output channels, - * as described in @ref opus_projection and - * @ref opus_projection_ctls. - * @param demixing_matrix_size opus_int32: The size in bytes of the - * demixing matrix, as - * described in @ref - * opus_projection_ctls. - * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) - * on failure. - */ -OPUS_EXPORT int opus_projection_decoder_init( - OpusProjectionDecoder *st, - opus_int32 Fs, - int channels, - int streams, - int coupled_streams, - unsigned char *demixing_matrix, - opus_int32 demixing_matrix_size -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); - - -/** Decode a projection Opus packet. - * @param st OpusProjectionDecoder*: Projection decoder state. - * @param[in] data const unsigned char*: Input payload. - * Use a NULL - * pointer to indicate packet - * loss. - * @param len opus_int32: Number of bytes in payload. - * @param[out] pcm opus_int16*: Output signal, with interleaved - * samples. - * This must contain room for - * frame_size*channels - * samples. - * @param frame_size int: The number of samples per channel of - * available space in \a pcm. - * If this is less than the maximum packet duration - * (120 ms; 5760 for 48kHz), this function will not be capable - * of decoding some packets. In the case of PLC (data==NULL) - * or FEC (decode_fec=1), then frame_size needs to be exactly - * the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the - * next incoming packet. For the PLC and FEC cases, frame_size - * must be a multiple of 2.5 ms. - * @param decode_fec int: Flag (0 or 1) to request that any in-band - * forward error correction data be decoded. - * If no such data is available, the frame is - * decoded as if it were lost. - * @returns Number of samples decoded on success or a negative error code - * (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode( - OpusProjectionDecoder *st, - const unsigned char *data, - opus_int32 len, - opus_int16 *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - - -/** Decode a projection Opus packet with floating point output. - * @param st OpusProjectionDecoder*: Projection decoder state. - * @param[in] data const unsigned char*: Input payload. - * Use a NULL - * pointer to indicate packet - * loss. - * @param len opus_int32: Number of bytes in payload. - * @param[out] pcm opus_int16*: Output signal, with interleaved - * samples. - * This must contain room for - * frame_size*channels - * samples. - * @param frame_size int: The number of samples per channel of - * available space in \a pcm. - * If this is less than the maximum packet duration - * (120 ms; 5760 for 48kHz), this function will not be capable - * of decoding some packets. In the case of PLC (data==NULL) - * or FEC (decode_fec=1), then frame_size needs to be exactly - * the duration of audio that is missing, otherwise the - * decoder will not be in the optimal state to decode the - * next incoming packet. For the PLC and FEC cases, frame_size - * must be a multiple of 2.5 ms. - * @param decode_fec int: Flag (0 or 1) to request that any in-band - * forward error correction data be decoded. - * If no such data is available, the frame is - * decoded as if it were lost. - * @returns Number of samples decoded on success or a negative error code - * (see @ref opus_errorcodes) on failure. - */ -OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode_float( - OpusProjectionDecoder *st, - const unsigned char *data, - opus_int32 len, - float *pcm, - int frame_size, - int decode_fec -) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); - - -/** Perform a CTL function on a projection Opus decoder. - * - * Generally the request and subsequent arguments are generated by a - * convenience macro. - * @param st OpusProjectionDecoder*: Projection decoder state. - * @param request This and all remaining parameters should be replaced by one - * of the convenience macros in @ref opus_genericctls, - * @ref opus_decoderctls, @ref opus_multistream_ctls, or - * @ref opus_projection_ctls. - * @see opus_genericctls - * @see opus_decoderctls - * @see opus_multistream_ctls - * @see opus_projection_ctls - */ -OPUS_EXPORT int opus_projection_decoder_ctl(OpusProjectionDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); - - -/** Frees an OpusProjectionDecoder allocated by - * opus_projection_decoder_create(). - * @param st OpusProjectionDecoder: Projection decoder state to be freed. - */ -OPUS_EXPORT void opus_projection_decoder_destroy(OpusProjectionDecoder *st); - - -/**@}*/ - -/**@}*/ - -#ifdef __cplusplus -} -#endif - -#endif /* OPUS_PROJECTION_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opus_types.h b/library/opusencoder/src/main/cpp/opus/include/opus_types.h deleted file mode 100644 index 7cf675580f..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opus_types.h +++ /dev/null @@ -1,166 +0,0 @@ -/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ -/* Modified by Jean-Marc Valin */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* opus_types.h based on ogg_types.h from libogg */ - -/** - @file opus_types.h - @brief Opus reference implementation types -*/ -#ifndef OPUS_TYPES_H -#define OPUS_TYPES_H - -#define opus_int int /* used for counters etc; at least 16 bits */ -#define opus_int64 long long -#define opus_int8 signed char - -#define opus_uint unsigned int /* used for counters etc; at least 16 bits */ -#define opus_uint64 unsigned long long -#define opus_uint8 unsigned char - -/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ -#if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) -#include -# undef opus_int64 -# undef opus_int8 -# undef opus_uint64 -# undef opus_uint8 - typedef int8_t opus_int8; - typedef uint8_t opus_uint8; - typedef int16_t opus_int16; - typedef uint16_t opus_uint16; - typedef int32_t opus_int32; - typedef uint32_t opus_uint32; - typedef int64_t opus_int64; - typedef uint64_t opus_uint64; -#elif defined(_WIN32) - -# if defined(__CYGWIN__) -# include <_G_config.h> - typedef _G_int32_t opus_int32; - typedef _G_uint32_t opus_uint32; - typedef _G_int16 opus_int16; - typedef _G_uint16 opus_uint16; -# elif defined(__MINGW32__) - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef int opus_int32; - typedef unsigned int opus_uint32; -# elif defined(__MWERKS__) - typedef int opus_int32; - typedef unsigned int opus_uint32; - typedef short opus_int16; - typedef unsigned short opus_uint16; -# else - /* MSVC/Borland */ - typedef __int32 opus_int32; - typedef unsigned __int32 opus_uint32; - typedef __int16 opus_int16; - typedef unsigned __int16 opus_uint16; -# endif - -#elif defined(__MACOS__) - -# include - typedef SInt16 opus_int16; - typedef UInt16 opus_uint16; - typedef SInt32 opus_int32; - typedef UInt32 opus_uint32; - -#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ - -# include - typedef int16_t opus_int16; - typedef u_int16_t opus_uint16; - typedef int32_t opus_int32; - typedef u_int32_t opus_uint32; - -#elif defined(__BEOS__) - - /* Be */ -# include - typedef int16 opus_int16; - typedef u_int16 opus_uint16; - typedef int32_t opus_int32; - typedef u_int32_t opus_uint32; - -#elif defined (__EMX__) - - /* OS/2 GCC */ - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef int opus_int32; - typedef unsigned int opus_uint32; - -#elif defined (DJGPP) - - /* DJGPP */ - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef int opus_int32; - typedef unsigned int opus_uint32; - -#elif defined(R5900) - - /* PS2 EE */ - typedef int opus_int32; - typedef unsigned opus_uint32; - typedef short opus_int16; - typedef unsigned short opus_uint16; - -#elif defined(__SYMBIAN32__) - - /* Symbian GCC */ - typedef signed short opus_int16; - typedef unsigned short opus_uint16; - typedef signed int opus_int32; - typedef unsigned int opus_uint32; - -#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) - - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef long opus_int32; - typedef unsigned long opus_uint32; - -#elif defined(CONFIG_TI_C6X) - - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef int opus_int32; - typedef unsigned int opus_uint32; - -#else - - /* Give up, take a reasonable guess */ - typedef short opus_int16; - typedef unsigned short opus_uint16; - typedef int opus_int32; - typedef unsigned int opus_uint32; - -#endif - -#endif /* OPUS_TYPES_H */ diff --git a/library/opusencoder/src/main/cpp/opus/include/opusenc.h b/library/opusencoder/src/main/cpp/opus/include/opusenc.h deleted file mode 100644 index ca1a5efe8e..0000000000 --- a/library/opusencoder/src/main/cpp/opus/include/opusenc.h +++ /dev/null @@ -1,358 +0,0 @@ -/* Copyright (c) 2017 Jean-Marc Valin */ -/* - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -#if !defined(_opusenc_h) -# define _opusenc_h (1) - -/**\mainpage - \section Introduction - - This is the documentation for the libopusenc C API. - - The libopusenc package provides a convenient high-level API for - encoding Ogg Opus files. - - \section Organization - - The main API is divided into several sections: - - \ref encoding - - \ref comments - - \ref encoder_ctl - - \ref callbacks - - \ref error_codes - - \section Overview - - The libopusfile API provides an easy way to encode Ogg Opus files using - libopus. -*/ - -# if defined(__cplusplus) -extern "C" { -# endif - -#include - -#ifndef OPE_EXPORT -# if defined(WIN32) -# if defined(OPE_BUILD) && defined(DLL_EXPORT) -# define OPE_EXPORT __declspec(dllexport) -# else -# define OPE_EXPORT -# endif -# elif defined(__GNUC__) && defined(OPE_BUILD) -# define OPE_EXPORT __attribute__ ((visibility ("default"))) -# else -# define OPE_EXPORT -# endif -#endif - -/**\defgroup error_codes Error Codes*/ -/*@{*/ -/**\name List of possible error codes - Many of the functions in this library return a negative error code when a - function fails. - This list provides a brief explanation of the common errors. - See each individual function for more details on what a specific error code - means in that context.*/ -/*@{*/ - - -/* Bump this when we change the API. */ -/** API version for this header. Can be used to check for features at compile time. */ -#define OPE_API_VERSION 0 - -#define OPE_OK 0 -/* Based on the relevant libopus code minus 10. */ -#define OPE_BAD_ARG -11 -#define OPE_INTERNAL_ERROR -13 -#define OPE_UNIMPLEMENTED -15 -#define OPE_ALLOC_FAIL -17 - -/* Specific to libopusenc. */ -#define OPE_CANNOT_OPEN -30 -#define OPE_TOO_LATE -31 -#define OPE_UNRECOVERABLE -32 -#define OPE_INVALID_PICTURE -33 -#define OPE_INVALID_ICON -34 -/*@}*/ -/*@}*/ - - -/* These are the "raw" request values -- they should usually not be used. */ -#define OPE_SET_DECISION_DELAY_REQUEST 14000 -#define OPE_GET_DECISION_DELAY_REQUEST 14001 -#define OPE_SET_MUXING_DELAY_REQUEST 14002 -#define OPE_GET_MUXING_DELAY_REQUEST 14003 -#define OPE_SET_COMMENT_PADDING_REQUEST 14004 -#define OPE_GET_COMMENT_PADDING_REQUEST 14005 -#define OPE_SET_SERIALNO_REQUEST 14006 -#define OPE_GET_SERIALNO_REQUEST 14007 -#define OPE_SET_PACKET_CALLBACK_REQUEST 14008 -/*#define OPE_GET_PACKET_CALLBACK_REQUEST 14009*/ -#define OPE_SET_HEADER_GAIN_REQUEST 14010 -#define OPE_GET_HEADER_GAIN_REQUEST 14011 - -/**\defgroup encoder_ctl Encoding Options*/ -/*@{*/ - -/**\name Control parameters - - Macros for setting encoder options.*/ -/*@{*/ - -#define OPE_SET_DECISION_DELAY(x) OPE_SET_DECISION_DELAY_REQUEST, __opus_check_int(x) -#define OPE_GET_DECISION_DELAY(x) OPE_GET_DECISION_DELAY_REQUEST, __opus_check_int_ptr(x) -#define OPE_SET_MUXING_DELAY(x) OPE_SET_MUXING_DELAY_REQUEST, __opus_check_int(x) -#define OPE_GET_MUXING_DELAY(x) OPE_GET_MUXING_DELAY_REQUEST, __opus_check_int_ptr(x) -#define OPE_SET_COMMENT_PADDING(x) OPE_SET_COMMENT_PADDING_REQUEST, __opus_check_int(x) -#define OPE_GET_COMMENT_PADDING(x) OPE_GET_COMMENT_PADDING_REQUEST, __opus_check_int_ptr(x) -#define OPE_SET_SERIALNO(x) OPE_SET_SERIALNO_REQUEST, __opus_check_int(x) -#define OPE_GET_SERIALNO(x) OPE_GET_SERIALNO_REQUEST, __opus_check_int_ptr(x) -/* FIXME: Add type-checking macros to these. */ -#define OPE_SET_PACKET_CALLBACK(x,u) OPE_SET_PACKET_CALLBACK_REQUEST, (x), (u) -/*#define OPE_GET_PACKET_CALLBACK(x,u) OPE_GET_PACKET_CALLBACK_REQUEST, (x), (u)*/ -#define OPE_SET_HEADER_GAIN(x,u) OPE_SET_HEADER_GAIN_REQUEST, __opus_check_int(x) -#define OPE_GET_HEADER_GAIN(x,u) OPE_GET_HEADER_GAIN_REQUEST, __opus_check_int_ptr(x) -/*@}*/ -/*@}*/ - -/**\defgroup callbacks Callback Functions */ -/*@{*/ - -/**\name Callback functions - - These are the callbacks that can be implemented for an encoder.*/ -/*@{*/ - -/** Called for writing a page. */ -typedef int (*ope_write_func)(void *user_data, const unsigned char *ptr, opus_int32 len); - -/** Called for closing a stream. */ -typedef int (*ope_close_func)(void *user_data); - -/** Called on every packet encoded (including header). */ -typedef int (*ope_packet_func)(void *user_data, const unsigned char *packet_ptr, opus_int32 packet_len, opus_uint32 flags); - -/** Callback functions for accessing the stream. */ -typedef struct { - /** Callback for writing to the stream. */ - ope_write_func write; - /** Callback for closing the stream. */ - ope_close_func close; -} OpusEncCallbacks; -/*@}*/ -/*@}*/ - -/** Opaque comments struct. */ -typedef struct OggOpusComments OggOpusComments; - -/** Opaque encoder struct. */ -typedef struct OggOpusEnc OggOpusEnc; - -/**\defgroup comments Comments Handling */ -/*@{*/ - -/**\name Functions for handling comments - - These functions make it possible to add comments and pictures to Ogg Opus files.*/ -/*@{*/ - -/** Create a new comments object. - \return Newly-created comments object. */ -OPE_EXPORT OggOpusComments *ope_comments_create(void); - -/** Create a deep copy of a comments object. - \param comments Comments object to copy - \return Deep copy of input. */ -OPE_EXPORT OggOpusComments *ope_comments_copy(OggOpusComments *comments); - -/** Destroys a comments object. - \param comments Comments object to destroy*/ -OPE_EXPORT void ope_comments_destroy(OggOpusComments *comments); - -/** Add a comment. - \param[in,out] comments Where to add the comments - \param tag Tag for the comment (must not contain = char) - \param val Value for the tag - \return Error code - */ -OPE_EXPORT int ope_comments_add(OggOpusComments *comments, const char *tag, const char *val); - -/** Add a comment as a single tag=value string. - \param[in,out] comments Where to add the comments - \param tag_and_val string of the form tag=value (must contain = char) - \return Error code - */ -OPE_EXPORT int ope_comments_add_string(OggOpusComments *comments, const char *tag_and_val); - -/** Add a picture. - \param[in,out] comments Where to add the comments - \param filename File name for the picture - \param picture_type Type of picture (-1 for default) - \param description Description (NULL means no comment) - \return Error code - */ -OPE_EXPORT int ope_comments_add_picture(OggOpusComments *comments, const char *filename, int picture_type, const char *description); - -/*@}*/ -/*@}*/ - -/**\defgroup encoding Encoding */ -/*@{*/ - -/**\name Functions for encoding Ogg Opus files - - These functions make it possible to encode Ogg Opus files.*/ -/*@{*/ - -/** Create a new OggOpus file. - \param path Path where to create the file - \param comments Comments associated with the stream - \param rate Input sampling rate (48 kHz is faster) - \param channels Number of channels - \param family Mapping family (0 for mono/stereo, 1 for surround) - \param[out] error Error code (NULL if no error is to be returned) - \return Newly-created encoder. - */ -OPE_EXPORT OggOpusEnc *ope_encoder_create_file(const char *path, OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); - -/** Create a new OggOpus stream to be handled using callbacks - \param callbacks Callback functions - \param user_data Pointer to be associated with the stream and passed to the callbacks - \param comments Comments associated with the stream - \param rate Input sampling rate (48 kHz is faster) - \param channels Number of channels - \param family Mapping family (0 for mono/stereo, 1 for surround) - \param[out] error Error code (NULL if no error is to be returned) - \return Newly-created encoder. - */ -OPE_EXPORT OggOpusEnc *ope_encoder_create_callbacks(const OpusEncCallbacks *callbacks, void *user_data, - OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); - -/** Create a new OggOpus stream to be used along with.ope_encoder_get_page(). - This is mostly useful for muxing with other streams. - \param comments Comments associated with the stream - \param rate Input sampling rate (48 kHz is faster) - \param channels Number of channels - \param family Mapping family (0 for mono/stereo, 1 for surround) - \param[out] error Error code (NULL if no error is to be returned) - \return Newly-created encoder. - */ -OPE_EXPORT OggOpusEnc *ope_encoder_create_pull(OggOpusComments *comments, opus_int32 rate, int channels, int family, int *error); - -/** Add/encode any number of float samples to the stream. - \param[in,out] enc Encoder - \param pcm Floating-point PCM values in the +/-1 range (interleaved if multiple channels) - \param samples_per_channel Number of samples for each channel - \return Error code*/ -OPE_EXPORT int ope_encoder_write_float(OggOpusEnc *enc, const float *pcm, int samples_per_channel); - -/** Add/encode any number of 16-bit linear samples to the stream. - \param[in,out] enc Encoder - \param pcm Linear 16-bit PCM values in the [-32768,32767] range (interleaved if multiple channels) - \param samples_per_channel Number of samples for each channel - \return Error code*/ -OPE_EXPORT int ope_encoder_write(OggOpusEnc *enc, const opus_int16 *pcm, int samples_per_channel); - -/** Get the next page from the stream (only if using ope_encoder_create_pull()). - \param[in,out] enc Encoder - \param[out] page Next available encoded page - \param[out] len Size (in bytes) of the page returned - \param flush If non-zero, forces a flush of the page (if any data avaiable) - \return 1 if there is a page available, 0 if not. */ -OPE_EXPORT int ope_encoder_get_page(OggOpusEnc *enc, unsigned char **page, opus_int32 *len, int flush); - -/** Finalizes the stream, but does not deallocate the object. - \param[in,out] enc Encoder - \return Error code - */ -OPE_EXPORT int ope_encoder_drain(OggOpusEnc *enc); - -/** Deallocates the obect. Make sure to ope_drain() first. - \param[in,out] enc Encoder - */ -OPE_EXPORT void ope_encoder_destroy(OggOpusEnc *enc); - -/** Ends the stream and create a new stream within the same file. - \param[in,out] enc Encoder - \param comments Comments associated with the stream - \return Error code - */ -OPE_EXPORT int ope_encoder_chain_current(OggOpusEnc *enc, OggOpusComments *comments); - -/** Ends the stream and create a new file. - \param[in,out] enc Encoder - \param path Path where to write the new file - \param comments Comments associated with the stream - \return Error code - */ -OPE_EXPORT int ope_encoder_continue_new_file(OggOpusEnc *enc, const char *path, OggOpusComments *comments); - -/** Ends the stream and create a new file (callback-based). - \param[in,out] enc Encoder - \param user_data Pointer to be associated with the new stream and passed to the callbacks - \param comments Comments associated with the stream - \return Error code - */ -OPE_EXPORT int ope_encoder_continue_new_callbacks(OggOpusEnc *enc, void *user_data, OggOpusComments *comments); - -/** Write out the header now rather than wait for audio to begin. - \param[in,out] enc Encoder - \return Error code - */ -OPE_EXPORT int ope_encoder_flush_header(OggOpusEnc *enc); - -/** Sets encoder options. - \param[in,out] enc Encoder - \param request Use a request macro - \return Error code - */ -OPE_EXPORT int ope_encoder_ctl(OggOpusEnc *enc, int request, ...); - -/** Converts a libopusenc error code into a human readable string. - * - * @param error Error number - * @returns Error string - */ -OPE_EXPORT const char *ope_strerror(int error); - -/** Returns a string representing the version of libopusenc being used at run time. - \return A string describing the version of this library */ -OPE_EXPORT const char *ope_get_version_string(void); - -/** ABI version for this header. Can be used to check for features at run time. - \return An integer representing the ABI version */ -OPE_EXPORT int ope_get_abi_version(void); - -/*@}*/ -/*@}*/ - -# if defined(__cplusplus) -} -# endif - -#endif diff --git a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so deleted file mode 100755 index 466c1df13a..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopus.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so deleted file mode 100755 index 9389efd919..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/arm64-v8a/libopusenc.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopus.so deleted file mode 100755 index d4bb6d427e..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopus.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so deleted file mode 100755 index 655f296d90..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/armeabi-v7a/libopusenc.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so deleted file mode 100755 index 0acf14474b..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/x86/libopus.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so deleted file mode 100755 index f862ea15f7..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/x86/libopusenc.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so b/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so deleted file mode 100755 index 1480c8c586..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopus.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopusenc.so b/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopusenc.so deleted file mode 100755 index f744374fe0..0000000000 Binary files a/library/opusencoder/src/main/cpp/opus/libs/x86_64/libopusenc.so and /dev/null differ diff --git a/library/opusencoder/src/main/cpp/opuscodec.cpp b/library/opusencoder/src/main/cpp/opuscodec.cpp deleted file mode 100644 index 51bd656c5d..0000000000 --- a/library/opusencoder/src/main/cpp/opuscodec.cpp +++ /dev/null @@ -1,42 +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. - */ - -#include -#include "codec/CodecOggOpus.h" - -CodecOggOpus oggCodec; - -extern "C" -JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_init(JNIEnv *env, jobject thiz, jstring file_path, jint sample_rate) { - char *path = (char*) env->GetStringUTFChars(file_path, 0); - return oggCodec.encoderInit(path, sample_rate); -} - -extern "C" -JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_writeFrame(JNIEnv *env, jobject thiz, jshortArray shorts, jint samples_per_channel) { - jshort *nativeShorts = env->GetShortArrayElements(shorts, 0); - return oggCodec.writeFrame((short *) nativeShorts, samples_per_channel); -} - -extern "C" -JNIEXPORT jint JNICALL Java_im_vector_opusencoder_OggOpusEncoder_setBitrate(JNIEnv *env, jobject thiz, jint bitrate) { - return oggCodec.setBitrate(bitrate); -} - -extern "C" -JNIEXPORT void JNICALL Java_im_vector_opusencoder_OggOpusEncoder_encoderRelease(JNIEnv *env, jobject thiz) { - oggCodec.encoderRelease(); -} diff --git a/library/opusencoder/src/main/cpp/utils/Logger.h b/library/opusencoder/src/main/cpp/utils/Logger.h deleted file mode 100644 index 9efdc51d41..0000000000 --- a/library/opusencoder/src/main/cpp/utils/Logger.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef ANDROIDOPUSENCODER_LOGGER_H -#define ANDROIDOPUSENCODER_LOGGER_H -#include - - -#define LOGE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__) -#define LOGW(tag, ...) __android_log_print(ANDROID_LOG_WARN, tag, __VA_ARGS__) -#define LOGI(tag, ...) __android_log_print(ANDROID_LOG_INFO, tag, __VA_ARGS__) -#define LOGD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__) - -#endif //ANDROIDOPUSENCODER_LOGGER_H diff --git a/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt b/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt deleted file mode 100644 index 8af11f8516..0000000000 --- a/library/opusencoder/src/main/java/im/vector/opusencoder/OggOpusEncoder.kt +++ /dev/null @@ -1,57 +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.opusencoder - -import android.util.Log -import androidx.annotation.IntRange -import im.vector.opusencoder.configuration.SampleRate - -/** - * JNI bridge to CodecOggOpus in the native opuscodec library. - */ -class OggOpusEncoder { - - companion object { - - private const val TAG = "OggOpusEncoder" - - init { - try { - System.loadLibrary("opuscodec") - } catch (e: Exception) { - Log.e(TAG, "Couldn't load opus library: $e") - } - } - } - - fun init(filePath: String, sampleRate: SampleRate): Int { - return init(filePath, sampleRate.value) - } - private external fun init(filePath: String, sampleRate: Int): Int - - external fun setBitrate(@IntRange(from = 500, to = 512000) bitrate: Int): Int - - fun encode(shorts: ShortArray, samplesPerChannel: Int): Int { - return writeFrame(shorts, samplesPerChannel) - } - private external fun writeFrame(shorts: ShortArray, samplesPerChannel: Int): Int - - fun release() { - encoderRelease() - } - private external fun encoderRelease() -} diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index b96e8ad76c..bc7756efbc 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -50,6 +50,7 @@ android { dependencies { implementation libs.androidx.appCompat + implementation libs.androidx.fragmentKtx implementation libs.google.material // Pref theme implementation libs.androidx.preferenceKtx diff --git a/library/ui-styles/src/debug/java/im/vector/lib/ui/styles/debug/DebugMaterialThemeActivity.kt b/library/ui-styles/src/debug/java/im/vector/lib/ui/styles/debug/DebugMaterialThemeActivity.kt index 553d495e22..412d6fdc1c 100644 --- a/library/ui-styles/src/debug/java/im/vector/lib/ui/styles/debug/DebugMaterialThemeActivity.kt +++ b/library/ui-styles/src/debug/java/im/vector/lib/ui/styles/debug/DebugMaterialThemeActivity.kt @@ -18,8 +18,12 @@ package im.vector.lib.ui.styles.debug import android.os.Bundle import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuProvider +import androidx.lifecycle.Lifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import im.vector.lib.ui.styles.R @@ -31,6 +35,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setupMenu() val views = ActivityDebugMaterialThemeBinding.inflate(layoutInflater) setContentView(views.root) @@ -72,6 +77,27 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { } } + private fun setupMenu() { + addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_debug, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + Toast.makeText( + this@DebugMaterialThemeActivity, + "Menu ${menuItem.title} clicked!", + Toast.LENGTH_SHORT + ).show() + return true + } + }, + this, + Lifecycle.State.RESUMED + ) + } + private fun showTestDialog(theme: Int) { MaterialAlertDialogBuilder(this, theme) .setTitle("Dialog title") @@ -82,9 +108,4 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { .setNeutralButton("Neutral", null) .show() } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_debug, menu) - return true - } } diff --git a/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml b/library/ui-styles/src/main/res/drawable/bg_gradient_ftue_breaker.xml similarity index 100% rename from library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml rename to library/ui-styles/src/main/res/drawable/bg_gradient_ftue_breaker.xml 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 7ac81b2d86..8be8e83569 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 @@ -53,6 +53,13 @@ class FlowRoom(private val room: Room) { } } + fun liveAreAllMembersLoaded(): Flow { + return room.membershipService().areAllMembersLoadedLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.membershipService().areAllMembersLoaded() + } + } + fun liveAnnotationSummary(eventId: String): Flow> { return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index d15113f5f2..ddd4cba1e0 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -17,7 +17,7 @@ buildscript { } } dependencies { - classpath "io.realm:realm-gradle-plugin:10.9.0" + classpath "io.realm:realm-gradle-plugin:10.11.0" } } @@ -60,7 +60,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.27\"" + buildConfigField "String", "SDK_VERSION", "\"1.4.28\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" 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 7dafe33935..a78953caac 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 @@ -18,12 +18,15 @@ 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.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -38,7 +41,10 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult 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.events.model.toModel +import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -47,6 +53,7 @@ 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 @@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit * This class exposes methods to be used in common cases * Registration, login, Sync, Sending messages... */ -class CommonTestHelper private constructor(context: Context) { +class CommonTestHelper internal constructor(context: Context) { companion object { internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { @@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) { 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") + } + } + } + } + + // not sure why it's taking so long :/ + runBlockingTest(90_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") + try { + otherSession.roomService().joinRoom(roomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + waitWithLatch { + retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(roomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } + /** * Reply in a thread * @param room the room where to send the messages @@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) { ) assertNotNull(session) return session.also { + // most of the test was created pre-MSC3061 so ensure compatibility + it.cryptoService().enableShareKeyOnInvite(false) trackedSessions.add(session) } } @@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) { * @param latch * @throws InterruptedException */ - fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { + fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) { assertTrue( "Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.", - latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS).also { + if (!it) { + // cancel job on timeout + job?.cancel("Await timeout") + } + } ) } suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { while (true) { - delay(1000) + try { + delay(1000) + } catch (ex: CancellationException) { + // the job was canceled, just stop + return + } if (condition()) { latch.countDown() return @@ -447,10 +497,10 @@ class CommonTestHelper private constructor(context: Context) { fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { val latch = CountDownLatch(1) - coroutineScope.launch(dispatcher) { + val job = coroutineScope.launch(dispatcher) { block(latch) } - await(latch, timeout) + await(latch, timeout, job) } fun runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { 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 5fd86d4fdb..f36bfb6210 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 @@ -53,6 +53,7 @@ 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 @@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { + 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 { name = "MyRoom" }) + aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = roomHistoryVisibility + name = "MyRoom" + }) } if (encryptedRoom) { testHelper.waitWithLatch { latch -> @@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId 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 new file mode 100644 index 0000000000..32d63a1934 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -0,0 +1,298 @@ +/* + * 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 android.util.Log +import androidx.test.filters.LargeTest +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +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.Session +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.common.CommonTestHelper +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) +@LargeTest +class E2EShareKeysConfigTest : InstrumentedTest { + + @Test + fun msc3061ShouldBeDisabledByDefault() = runCryptoTest(context()) { _, commonTestHelper -> + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + Assert.assertFalse("MSC3061 is lab and should be disabled by default", aliceSession.cryptoService().isShareKeysOnInviteEnabled()) + } + + @Test + 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 roomAlice = aliceSession.roomService().getRoom(roomId)!! + + // send some messages + val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + aliceSession.cryptoService().discardOutboundSession(roomId) + val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + + // Create bob account + val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) + + // Let alice invite bob + commonTestHelper.runBlockingTest { + roomAlice.membershipService().invite(bobSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) + + // Bob has join but should not be able to decrypt history + cryptoTestHelper.ensureCannotDecrypt( + withSession1.map { it.eventId } + withSession2.map { it.eventId }, + bobSession, + roomId + ) + + // We don't need bob anymore + commonTestHelper.signOutAndClose(bobSession) + + // Now let's enable history key sharing on alice side + aliceSession.cryptoService().enableShareKeyOnInvite(true) + + // let's add a new message first + val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1) + + // Worth nothing to check that the session was rotated + Assert.assertNotEquals( + "Session should have been rotated", + withSession2.first().root.content?.get("session_id")!!, + afterFlagOn.first().root.content?.get("session_id")!! + ) + + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + commonTestHelper.runBlockingTest { + roomAlice.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session + cryptoTestHelper.ensureCannotDecrypt( + withSession1.map { it.eventId } + withSession2.map { it.eventId }, + samSession, + roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + afterFlagOn.map { it.eventId }, + samSession, + roomId, + afterFlagOn.map { it.root.getClearContent()?.get("body") as String }) + } + + @Test + fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + val aliceSession = testData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(false) + } + val bobSession = testData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + + val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession) + + // Bob should have shared history keys to sam. + // But has alice hasn't enabled sharing, bob shouldn't send her sessions + cryptoTestHelper.ensureCannotDecrypt( + fromAliceNotSharable.map { it.eventId }, + samSession, + testData.roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + fromBobSharable.map { it.eventId }, + samSession, + testData.roomId, + fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) + } + + @Test + fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + val aliceSession = testData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val bobSession = testData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + + val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession) + + cryptoTestHelper.ensureCanDecrypt( + fromAliceNotSharable.map { it.eventId }, + samSession, + testData.roomId, + fromAliceNotSharable.map { it.root.getClearContent()?.get("body") as String }) + + cryptoTestHelper.ensureCanDecrypt( + fromBobSharable.map { it.eventId }, + samSession, + testData.roomId, + fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) + } + + private 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) + + // Now let bob invite Sam + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let bob invite sam + commonTestHelper.runBlockingTest { + bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId) + return Triple(fromAliceNotSharable, fromBobSharable, samSession) + } + + // test flag on backup is correct + + @Test + 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 roomAlice = aliceSession.roomService().getRoom(roomId)!! + + // send some messages + val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + aliceSession.cryptoService().enableShareKeyOnInvite(true) + val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + + Log.v("#E2E TEST", "Create and start key backup for bob ...") + val keysBackupService = aliceSession.cryptoService().keysBackupService() + val keyBackupPassword = "FooBarBaz" + val megolmBackupCreationInfo = commonTestHelper.doSync { + keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) + } + val version = commonTestHelper.doSync { + keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + commonTestHelper.waitWithLatch { latch -> + keysBackupService.backupAllGroupSessions( + null, + TestMatrixCallback(latch, true) + ) + } + + // signout + commonTestHelper.signOutAndClose(aliceSession) + + val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + newAliceSession.cryptoService().enableShareKeyOnInvite(true) + + newAliceSession.cryptoService().keysBackupService().let { kbs -> + val keyVersionResult = commonTestHelper.doSync { + kbs.getVersion(version.version, it) + } + + val importedResult = commonTestHelper.doSync { + kbs.restoreKeyBackupWithPassword( + keyVersionResult!!, + keyBackupPassword, + null, + null, + null, + it + ) + } + + assertEquals(2, importedResult.totalNumberOfKeys) + } + + // Now let's invite sam + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + commonTestHelper.runBlockingTest { + newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session + cryptoTestHelper.ensureCannotDecrypt( + notSharableMessage.map { it.eventId }, + samSession, + roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + sharableMessage.map { it.eventId }, + samSession, + roomId, + sharableMessage.map { it.root.getClearContent()?.get("body") as String }) + } +} 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 5a61eee7fe..251c13ccbf 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 @@ -23,7 +23,6 @@ import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -49,9 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.mustFail import java.util.concurrent.CountDownLatch +// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest -@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") class E2eeSanityTests : InstrumentedTest { @get:Rule val rule = RetryTestRule(3) @@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest { // All user should accept invite otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) + testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") } @@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) + testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID) } ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) @@ -740,37 +737,6 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } - } - } - } - - // not sure why it's taking so long :/ - testHelper.runBlockingTest(90_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.roomService().joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } - } - - Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } - } - } - private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> sentEventIds.forEach { sentEventId -> 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 new file mode 100644 index 0000000000..32a95008b1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -0,0 +1,424 @@ +/* + * 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 android.util.Log +import androidx.test.filters.LargeTest +import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.internal.assertNotEquals +import org.junit.Assert +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.query.QueryStringValue +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.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.failure.JoinRoomFailure +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.RoomHistoryVisibilityContent +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 + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeShareKeysHistoryTest : InstrumentedTest { + + @Test + fun testShareMessagesHistoryWithRoomWorldReadable() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE) + } + + @Test + fun testShareMessagesHistoryWithRoomShared() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED) + } + + @Test + fun testShareMessagesHistoryWithRoomJoined() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED) + } + + @Test + fun testShareMessagesHistoryWithRoomInvited() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED) + } + + /** + * In this test we create a room and test that new members + * can decrypt history when the room visibility is + * RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE. + * We should not be able to view messages/decrypt otherwise + */ + private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = + runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) + + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! + + // Bob + val bobSession = cryptoTestData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!! + + 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) + 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) { + val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") + } + } + } + } + + // Create a new user + val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also { + it.cryptoService().enableShareKeyOnInvite(true) + } + 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) + } + + waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) + + ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper) + Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID") + + when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE, + RoomHistoryVisibility.SHARED, + null + -> { + // Aris should be able to decrypt the message + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE + ).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.signOutAndClose(arisSession) + cryptoTestData.cleanUp(testHelper) + } + + @Test + fun testNeedsRotationFromWorldReadableToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromWorldReadableToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromWorldReadableToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromSharedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable")) + } + + @Test + fun testNeedsRotationFromSharedToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromSharedToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromInvitedToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromInvitedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable")) + } + + @Test + fun testNeedsRotationFromInvitedToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromJoinedToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromJoinedToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromJoinedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable")) + } + + /** + * In this test we will test that a rotation is needed when + * When the room's history visibility setting changes to world_readable or shared + * from invited or joined, or changes to invited or joined from world_readable or shared, + * senders that support this flag must rotate their megolm sessions. + */ + private fun testRotationDueToVisibilityChange( + initRoomHistoryVisibility: RoomHistoryVisibility, + nextRoomHistoryVisibility: RoomHistoryVisibilityContent + ) { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! +// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + + // Bob + val bobSession = cryptoTestData.secondSession!! + + val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!! + + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID") + + val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) + Assert.assertTrue("Message should be sent", aliceMessageId != null) + Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID") + + // Bob should be able to decrypt the message + var firstAliceMessageMegolmSessionId: String? = null + val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + 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.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() + ) + } + + // 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.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 + } + } + + 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 + } + } + } + } + } + + when { + initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> { + assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId) + Log.v("#E2E TEST ROTATION", "Rotation is not needed") + } + initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> { + assertNotEquals("Session should have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId) + Log.v("#E2E TEST ROTATION", "Rotation is needed!") + } + } + + cryptoTestData.cleanUp(testHelper) + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { + return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.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 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") + } + } + } + } + + testHelper.runBlockingTest(60_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.roomService().joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + 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 + } + } + } +} 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 e37ae5be86..e8e7b1d708 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 @@ -72,7 +72,7 @@ class PreShareKeysTest : InstrumentedTest { assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) - val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier() + val megolmSessionId = bobInboundForAlice.session.sessionIdentifier() assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) 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 45fdb9e1e3..cf201611a0 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 @@ -19,14 +19,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestData -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper /** * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] */ internal data class KeysBackupScenarioData( val cryptoTestData: CryptoTestData, - val aliceKeys: List, + val aliceKeys: List, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { 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 fb498e0de5..e160938721 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 @@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest { val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo // - Check encryptGroupSession() returns stg - val keyBackupData = keysBackup.encryptGroupSession(session) + val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } assertNotNull(keyBackupData) assertNotNull(keyBackupData!!.sessionData) @@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest { val sessionData = keysBackup .decryptKeyBackupData( keyBackupData, - session.olmInboundGroupSession!!.sessionIdentifier(), + session.safeSessionId!!, cryptoTestData.roomId, decryption!! ) 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 38f94c5103..2cc2b506b9 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 @@ -187,7 +187,7 @@ internal class KeysBackupTestHelper( // - Alice must have the same keys on both devices for (aliceKey1 in testData.aliceKeys) { val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store - .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!) + .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!) Assert.assertNotNull(aliceKey2) assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) } 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 38136ff5ce..2cd579df24 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 @@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest { val roomName = "My Space" val topic = "A public space for test" var spaceId: String = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceId = session.spaceService().createSpace(roomName, topic, null, true) - // wait a bit to let the summary update it self :/ - it.countDown() } - Thread.sleep(4_000) - val syncedSpace = session.spaceService().getSpace(spaceId) commonTestHelper.waitWithLatch { commonTestHelper.retryPeriodicallyWithLatch(it) { - syncedSpace?.asRoom()?.roomSummary()?.name != null + session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null } } + + val syncedSpace = session.spaceService().getSpace(spaceId) assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name) assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic) // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") 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 63ca963479..80020665f8 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 @@ -20,7 +20,6 @@ import android.util.Log import androidx.lifecycle.Observer import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Ignore @@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest { val spaceName = "My Space" val topic = "A public space for test" var spaceId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceId = session.spaceService().createSpace(spaceName, topic, null, true) - it.countDown() } val syncedSpace = session.spaceService().getSpace(spaceId) var roomId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) - it.countDown() } val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { syncedSpace!!.addChildren(roomId, viaServers, null, true) - it.countDown() } - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - it.countDown() } - Thread.sleep(9000) - - 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") + 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 + } } - - assertNotNull(parents) - assertEquals(1, parents!!.size) - assertEquals(spaceName, parents.first().roomSummary?.name) - - assertNotNull(canonicalParents) - assertEquals(1, canonicalParents!!.size) - assertEquals(spaceName, canonicalParents.first().roomSummary?.name) } // @Test @@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest { // } @Test - fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) /* val spaceBInfo = */ createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - it.countDown() } // Create orphan rooms var orphan1 = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) - it.countDown() } var orphan2 = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) - it.countDown() } val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) @@ -240,10 +235,9 @@ 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.waitWithLatch { + commonTestHelper.runBlockingTest { val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) spaceA!!.addChildren(a3, viaServers, null, false) - it.countDown() } Thread.sleep(6_000) @@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest { @Test @Ignore("This test will be ignored until it is fixed") - fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testBreakCycle() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - it.countDown() } // add back A as subspace of C - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) - it.countDown() } // A -> C -> A @@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test - fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, + "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceBInfo = createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, + "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - runBlocking { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) } val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, + "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) commonTestHelper.waitWithLatch { latch -> @@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest { } } + 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 - - flatAChildren.observeForever(childObserver) } // Test part one of the rooms @@ -374,10 +379,10 @@ class SpaceHierarchyTest : InstrumentedTest { } } - // 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) } @@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest { ) private fun createPublicSpace( + commonTestHelper: CommonTestHelper, session: Session, spaceName: String, childInfo: List> @@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest { ): TestSpaceCreationResult { var spaceId = "" var roomIds: List = emptyList() - runSessionTest(context()) { commonTestHelper -> - commonTestHelper.waitWithLatch { latch -> - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + 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 = 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) } - 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) - } - } - latch.countDown() } } return TestSpaceCreationResult(spaceId, roomIds) } private fun createPrivateSpace( + commonTestHelper: CommonTestHelper, session: Session, spaceName: String, childInfo: List> @@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest { ): TestSpaceCreationResult { var spaceId = "" var roomIds: List = emptyList() - runSessionTest(context()) { commonTestHelper -> - commonTestHelper.waitWithLatch { latch -> - 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) + 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) } - latch.countDown() } } return TestSpaceCreationResult(spaceId, roomIds) @@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceBInfo = createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") @@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest { runBlocking { val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - Thread.sleep(6_000) } // Thread.sleep(4_000) @@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest { // + C // + c1, c2 - val rootSpaces = commonTestHelper.runBlockingTest { - session.spaceService().getRootSpaceSummaries() + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } + rootSpaces.size == 2 + } } - - assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) } @Test @@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val spaceAInfo = createPrivateSpace( - aliceSession, "Private Space A", listOf( - Triple("General", true /*suggested*/, true/*canonical*/), - Triple("Random", true, true) - ) + commonTestHelper, + aliceSession, "Private Space A", + listOf( + Triple("General", true /*suggested*/, true/*canonical*/), + Triple("Random", true, true) + ) ) commonTestHelper.runBlockingTest { @@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest { } var bobRoomId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - it.countDown() } commonTestHelper.runBlockingTest { @@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest { } } - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - it.countDown() } commonTestHelper.waitWithLatch { latch -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt new file mode 100644 index 0000000000..627a825679 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt @@ -0,0 +1,38 @@ +/* + * 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.auth + +enum class LoginType { + PASSWORD, + SSO, + UNSUPPORTED, + CUSTOM, + DIRECT, + UNKNOWN; + + companion object { + + fun fromName(name: String) = when (name) { + PASSWORD.name -> PASSWORD + SSO.name -> SSO + UNSUPPORTED.name -> UNSUPPORTED + CUSTOM.name -> CUSTOM + DIRECT.name -> DIRECT + else -> UNKNOWN + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt index e3815231d9..de227631ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.auth.data +import org.matrix.android.sdk.api.auth.LoginType + /** * This data class holds necessary data to open a session. * You don't have to manually instantiate it. @@ -34,7 +36,12 @@ data class SessionParams( /** * Set to false if the current token is not valid anymore. Application should not have to use this info. */ - val isTokenValid: Boolean + val isTokenValid: Boolean, + + /** + * The authentication method that was used to create the session. + */ + val loginType: LoginType, ) { /* * Shortcuts. Usually the application should only need to use these shortcuts 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 9507ddda65..015cb6a1a2 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 @@ -38,4 +38,5 @@ data class MXCryptoConfig constructor( * You can limit request only to your sessions by turning this setting to `true` */ val limitRoomKeyRequestsToMyDevices: Boolean = false, -) + + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt index baf33a59c5..f5e5628566 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt @@ -162,7 +162,7 @@ enum class ApiPath(val path: String, val method: String) { KICK_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick", "POST"), REDACT_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}", "PUT"), REPORT_CONTENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}", "POST"), - GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases", "GET"), + GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/aliases", "GET"), SEND_TYPING_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}", "PUT"), PUT_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "PUT"), DELETE_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "DELETE"), 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 638da11804..a5e05f69e0 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 @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.SessionInfo interface CryptoService { @@ -84,6 +85,20 @@ interface CryptoService { fun isKeyGossipingEnabled(): Boolean + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun enableShareKeyOnInvite(enable: Boolean) + + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun isShareKeysOnInviteEnabled(): Boolean + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -176,4 +191,9 @@ interface CryptoService { * send, in order to speed up sending of the message. */ fun prepareToEncrypt(roomId: String, callback: MatrixCallback) + + /** + * Share all inbound sessions of the last chunk messages to the provided userId devices. + */ + suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt index 3df4ef7c9a..664cd00e94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt @@ -69,5 +69,11 @@ data class ForwardedRoomKeyContent( * private part of this key unless they have done device verification. */ @Json(name = "sender_claimed_ed25519_key") - val senderClaimedEd25519Key: String? = null + val senderClaimedEd25519Key: String? = null, + + /** + * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean? = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt index 0830a566ab..5b18d29ea0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt @@ -38,5 +38,12 @@ data class RoomKeyContent( // should be a Long but it is sometimes a double @Json(name = "chain_index") - val chainIndex: Any? = null + val chainIndex: Any? = null, + + /** + * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean? = false + ) 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 5dfb8961e3..90f3f323b2 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 @@ -40,6 +40,18 @@ interface RoomService { */ suspend fun createRoom(createRoomParams: CreateRoomParams): String + /** + * Create a room locally. + * This room will not be synchronized with the server and will not come back from the sync, so all the events related to this room will be generated + * locally. + */ + suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String + + /** + * Delete a local room with all its related events. + */ + suspend fun deleteLocalRoom(roomId: String) + /** * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index ada3dc85d7..14095b67c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -48,9 +48,10 @@ interface LocationSharingService { /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds + * @param description description of the live for text fallback * @return the result of the update of the live */ - suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult + suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult /** * Stops sharing live location in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt index e7ac69be74..144cfeb3b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -30,6 +30,20 @@ interface MembershipService { */ suspend fun loadRoomMembersIfNeeded() + /** + * All the room members can be not loaded, for instance after an initial sync. + * All the members will be loaded when calling [loadRoomMembersIfNeeded], or when sending an encrypted + * event to the room. + * The fun let the app know if all the members have been loaded for this room. + * @return true if all the members are loaded, or false elsewhere. + */ + suspend fun areAllMembersLoaded(): Boolean + + /** + * Live version for [areAllMembersLoaded]. + */ + fun areAllMembersLoadedLive(): LiveData + /** * Return the roomMember with userId or null. * @param userId the userId param to look for diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt index 06069f2646..2b0ea1d8fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt @@ -48,3 +48,9 @@ enum class RoomHistoryVisibility { */ @Json(name = "joined") JOINED } + +/** + * Room history should be shared only if room visibility is world_readable or shared. + */ +internal fun RoomHistoryVisibility.shouldShareHistory() = + this == RoomHistoryVisibility.WORLD_READABLE || this == RoomHistoryVisibility.SHARED 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 new file mode 100644 index 0000000000..7ef0d63924 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt @@ -0,0 +1,31 @@ +/* + * 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.localecho + +import java.util.UUID + +object RoomLocalEcho { + + private const val PREFIX = "!local." + + /** + * Tell whether the provider room id is a local id. + */ + fun isLocalEchoId(roomId: String) = roomId.startsWith(PREFIX) + + internal fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index ef47775f1b..5ec0dedadf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -33,9 +33,15 @@ object MimeTypes { const val Ogg = "audio/ogg" + const val PlainText = "text/plain" + fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse() fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse() fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse() + fun String?.isMimeTypeApplication() = this?.startsWith("application/").orFalse() + fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse() + fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse() + fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt index ddb70be906..463692e574 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -83,6 +83,9 @@ internal abstract class AuthModule { @Binds abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + @Binds + abstract fun bindSessionParamsCreator(creator: DefaultSessionParamsCreator): SessionParamsCreator + @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 9d6b018a67..446f931847 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -22,6 +22,7 @@ import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -361,7 +362,7 @@ internal class DefaultAuthenticationService @Inject constructor( homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials ): Session { - return sessionCreator.createSession(credentials, homeServerConnectionConfig) + return sessionCreator.createSession(credentials, homeServerConnectionConfig, LoginType.SSO) } override suspend fun getWellKnownData( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt index ba01146a4a..7dbb11c7fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt @@ -16,69 +16,41 @@ package org.matrix.android.sdk.internal.auth -import android.net.Uri +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.SessionManager -import timber.log.Timber import javax.inject.Inject internal interface SessionCreator { - suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session + + suspend fun createSession( + credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + loginType: LoginType, + ): Session } internal class DefaultSessionCreator @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val pendingSessionStore: PendingSessionStore, - private val isValidClientServerApiTask: IsValidClientServerApiTask + private val sessionParamsCreator: SessionParamsCreator, ) : SessionCreator { /** * Credentials can affect the homeServerConnectionConfig, override homeserver url and/or * identity server url if provided in the credentials. */ - override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + override suspend fun createSession( + credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + loginType: LoginType, + ): Session { // We can cleanup the pending session params pendingSessionStore.delete() - - val overriddenUrl = credentials.discoveryInformation?.homeServer?.baseURL - // remove trailing "/" - ?.trim { it == '/' } - ?.takeIf { it.isNotBlank() } - // It can be the same value, so in this case, do not check again the validity - ?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() } - ?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") } - ?.let { Uri.parse(it) } - ?.takeIf { - // Validate the URL, if the configuration is wrong server side, do not override - tryOrNull { - isValidClientServerApiTask.execute( - IsValidClientServerApiTask.Params( - homeServerConnectionConfig.copy(homeServerUriBase = it) - ) - ) - .also { Timber.d("Overriding homeserver url: $it") } - } ?: true // In case of other error (no network, etc.), consider it is valid... - } - - val sessionParams = SessionParams( - credentials = credentials, - homeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUriBase = overriddenUrl ?: homeServerConnectionConfig.homeServerUriBase, - identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL - // remove trailing "/" - ?.trim { it == '/' } - ?.takeIf { it.isNotBlank() } - ?.also { Timber.d("Overriding identity server url to $it") } - ?.let { Uri.parse(it) } - ?: homeServerConnectionConfig.identityServerUri - ), - isTokenValid = true) - + val sessionParams = sessionParamsCreator.create(credentials, homeServerConnectionConfig, loginType) sessionParamsStore.save(sessionParams) return sessionManager.getOrCreateSession(sessionParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt new file mode 100644 index 0000000000..31ed9a1e85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt @@ -0,0 +1,83 @@ +/* + * 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.auth + +import android.net.Uri +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.extensions.tryOrNull +import timber.log.Timber +import javax.inject.Inject + +internal interface SessionParamsCreator { + + suspend fun create( + credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + loginType: LoginType, + ): SessionParams +} + +internal class DefaultSessionParamsCreator @Inject constructor( + private val isValidClientServerApiTask: IsValidClientServerApiTask +) : SessionParamsCreator { + + override suspend fun create( + credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + loginType: LoginType, + ) = SessionParams( + credentials = credentials, + homeServerConnectionConfig = homeServerConnectionConfig.overrideWithCredentials(credentials), + isTokenValid = true, + loginType = loginType, + ) + + private suspend fun HomeServerConnectionConfig.overrideWithCredentials(credentials: Credentials) = copy( + homeServerUriBase = credentials.getHomeServerUri(this) ?: homeServerUriBase, + identityServerUri = credentials.getIdentityServerUri() ?: identityServerUri + ) + + private suspend fun Credentials.getHomeServerUri(homeServerConnectionConfig: HomeServerConnectionConfig) = + discoveryInformation?.homeServer?.baseURL + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + // It can be the same value, so in this case, do not check again the validity + ?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() } + ?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") } + ?.let { Uri.parse(it) } + ?.takeIf { validateUri(it, homeServerConnectionConfig) } + + private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) = + // Validate the URL, if the configuration is wrong server side, do not override + tryOrNull { + performClientServerApiValidation(uri, homeServerConnectionConfig) + } ?: true // In case of other error (no network, etc.), consider it is valid... + + private suspend fun performClientServerApiValidation(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) = + isValidClientServerApiTask.execute( + IsValidClientServerApiTask.Params(homeServerConnectionConfig.copy(homeServerUriBase = uri)) + ).also { Timber.d("Overriding homeserver url: $it") } + + private fun Credentials.getIdentityServerUri() = discoveryInformation?.identityServer?.baseURL + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + ?.also { Timber.d("Overriding identity server url to $it") } + ?.let { Uri.parse(it) } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt index 88c6d04ee6..0bc7831f5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo001 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo002 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo004 +import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo005 import timber.log.Timber import javax.inject.Inject @@ -33,7 +34,7 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration { override fun equals(other: Any?) = other is AuthRealmMigration override fun hashCode() = 4000 - val schemaVersion = 4L + val schemaVersion = 5L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") @@ -42,5 +43,6 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration { if (oldVersion < 2) MigrateAuthTo002(realm).perform() if (oldVersion < 3) MigrateAuthTo003(realm).perform() if (oldVersion < 4) MigrateAuthTo004(realm).perform() + if (oldVersion < 5) MigrateAuthTo005(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt index ba1ab8147b..f6c883cac0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt @@ -26,5 +26,6 @@ internal open class SessionParamsEntity( var homeServerConnectionConfigJson: String = "", // Set to false when the token is invalid and the user has been soft logged out // In case of hard logout, this object is deleted from DB - var isTokenValid: Boolean = true + var isTokenValid: Boolean = true, + var loginType: String = "", ) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt index 86929b1afe..23923bf267 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.db import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SessionParams @@ -37,7 +38,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { if (credentials == null || homeServerConnectionConfig == null) { return null } - return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid) + return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid, LoginType.fromName(entity.loginType)) } fun map(sessionParams: SessionParams?): SessionParamsEntity? { @@ -54,7 +55,8 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { sessionParams.userId, credentialsJson, homeServerConnectionConfigJson, - sessionParams.isTokenValid + sessionParams.isTokenValid, + sessionParams.loginType.name, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt new file mode 100644 index 0000000000..2cf1b62a4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt @@ -0,0 +1,35 @@ +/* + * 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.auth.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import timber.log.Timber + +internal class MigrateAuthTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { + + override fun doMigrate(realm: DynamicRealm) { + Timber.d("Update SessionParamsEntity to add LoginType") + + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java) + ?.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true) + ?.transform { it.set(SessionParamsEntityFields.LOGIN_TYPE, LoginType.UNKNOWN.name) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 656a4f671b..468e998407 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.login import android.util.Patterns +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid @@ -78,7 +79,7 @@ internal class DefaultLoginWizard( authAPI.login(loginParams) } - return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.PASSWORD) } /** @@ -92,7 +93,7 @@ internal class DefaultLoginWizard( authAPI.login(loginParams) } - return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.SSO) } override suspend fun loginCustom(data: JsonDict): Session { @@ -100,7 +101,7 @@ internal class DefaultLoginWizard( authAPI.login(data) } - return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.CUSTOM) } override suspend fun resetPassword(email: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt index c9311867c8..af42105756 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.login import dagger.Lazy import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session @@ -77,7 +78,7 @@ internal class DefaultDirectLoginTask @Inject constructor( } } - return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig, LoginType.DIRECT) } private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index d6ec0297b4..56425cbc74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.registration import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegisterThreePid @@ -36,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData * This class execute the registration request and is responsible to keep the session of interactive authentication. */ internal class DefaultRegistrationWizard( - authAPI: AuthAPI, - private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + authAPI: AuthAPI, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -64,7 +65,7 @@ internal class DefaultRegistrationWizard( override suspend fun getRegistrationFlow(): RegistrationResult { val params = RegistrationParams() - return performRegistrationRequest(params) + return performRegistrationRequest(params, LoginType.PASSWORD) } override suspend fun createAccount( @@ -73,43 +74,43 @@ internal class DefaultRegistrationWizard( initialDeviceDisplayName: String? ): RegistrationResult { val params = RegistrationParams( - username = userName, - password = password, - initialDeviceDisplayName = initialDeviceDisplayName + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName ) - return performRegistrationRequest(params) - .also { - pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) - .also { pendingSessionStore.savePendingSessionData(it) } - } + return performRegistrationRequest(params, LoginType.PASSWORD) + .also { + pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) + .also { pendingSessionStore.savePendingSessionData(it) } + } } override suspend fun performReCaptcha(response: String): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) - return performRegistrationRequest(params) + return performRegistrationRequest(params, LoginType.PASSWORD) } override suspend fun acceptTerms(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) - return performRegistrationRequest(params) + return performRegistrationRequest(params, LoginType.PASSWORD) } override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } return sendThreePid(threePid) } override suspend fun sendAgainThreePid(): RegistrationResult { val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") return sendThreePid(safeCurrentThreePid) } @@ -125,7 +126,7 @@ internal class DefaultRegistrationWizard( ) pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } val params = RegistrationParams( auth = if (threePid is RegisterThreePid.Email) { @@ -148,17 +149,17 @@ internal class DefaultRegistrationWizard( ) // Store data pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } // and send the sid a first time - return performRegistrationRequest(params) + return performRegistrationRequest(params, LoginType.PASSWORD) } override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult { val safeParam = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") - return performRegistrationRequest(safeParam, delayMillis) + return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis) } override suspend fun handleValidateThreePid(code: String): RegistrationResult { @@ -167,19 +168,19 @@ internal class DefaultRegistrationWizard( private suspend fun validateThreePid(code: String): RegistrationResult { val registrationParams = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code") val validationBody = ValidationCodeBody( - clientSecret = pendingSessionData.clientSecret, - sid = safeCurrentData.addThreePidRegistrationResponse.sid, - code = code + clientSecret = pendingSessionData.clientSecret, + sid = safeCurrentData.addThreePidRegistrationResponse.sid, + code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) if (validationResponse.isSuccess()) { // The entered code is correct // Same than validate email - return performRegistrationRequest(registrationParams, 3_000) + return performRegistrationRequest(registrationParams, LoginType.PASSWORD, 3_000) } else { // The code is not correct throw Failure.SuccessError @@ -188,10 +189,10 @@ internal class DefaultRegistrationWizard( override suspend fun dummy(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) - return performRegistrationRequest(params) + return performRegistrationRequest(params, LoginType.PASSWORD) } override suspend fun registrationCustom( @@ -204,25 +205,28 @@ internal class DefaultRegistrationWizard( mutableParams["session"] = safeSession val params = RegistrationCustomParams(auth = mutableParams) - return performRegistrationOtherRequest(params) + return performRegistrationOtherRequest(LoginType.CUSTOM, params) } private suspend fun performRegistrationRequest( registrationParams: RegistrationParams, + loginType: LoginType, delayMillis: Long = 0 ): RegistrationResult { delay(delayMillis) - return register { registerTask.execute(RegisterTask.Params(registrationParams)) } + return register(loginType) { registerTask.execute(RegisterTask.Params(registrationParams)) } } private suspend fun performRegistrationOtherRequest( - registrationCustomParams: RegistrationCustomParams + loginType: LoginType, + registrationCustomParams: RegistrationCustomParams, ): RegistrationResult { - return register { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) } + return register(loginType) { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) } } private suspend fun register( - execute: suspend () -> Credentials + loginType: LoginType, + execute: suspend () -> Credentials, ): RegistrationResult { val credentials = try { execute.invoke() @@ -237,8 +241,7 @@ internal class DefaultRegistrationWizard( } } - val session = - sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, loginType) return RegistrationResult.Success(session) } 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 e0bcde2296..850a4379ca 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 @@ -71,6 +71,7 @@ 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.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction @@ -81,6 +82,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFact import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE +import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -963,8 +965,12 @@ internal class DefaultCryptoService @Inject constructor( private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { if (!event.isStateEvent()) return val eventContent = event.content.toModel() - eventContent?.historyVisibility?.let { - cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) + val historyVisibility = eventContent?.historyVisibility + if (historyVisibility == null) { + cryptoStore.setShouldShareHistory(roomId, false) + } else { + cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) + cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) } } @@ -1111,6 +1117,10 @@ internal class DefaultCryptoService @Inject constructor( override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() + override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled() + + override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable) + /** * Tells whether the client should ever send encrypted messages to unverified devices. * The default value is false. @@ -1335,6 +1345,30 @@ internal class DefaultCryptoService @Inject constructor( } } + override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) { + deviceListManager.downloadKeys(listOf(userId), false) + val userDevices = cryptoStore.getUserDeviceList(userId) + val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo -> + // Get inbound session from sessionId and sessionKey + withContext(coroutineDispatchers.crypto) { + olmDevice.getInboundGroupSession( + sessionId = sessionInfo.sessionId, + senderKey = sessionInfo.senderKey, + roomId = roomId + ).takeIf { it.wrapper.sessionData.sharedHistory } + } + } + + userDevices?.forEach { deviceInfo -> + // Lets share the provided inbound sessions for every user device + sessionToShare.forEach { inboundGroupSession -> + val encryptor = roomEncryptorsStore.get(roomId) + encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo) + Timber.i("## CRYPTO | Sharing inbound session") + } + } + } + /* ========================================================================================== * For test only * ========================================================================================== */ 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 e4d322cadd..ab7cbb74b1 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 @@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.Mutex import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber import java.util.Timer @@ -31,7 +31,7 @@ import java.util.TimerTask import javax.inject.Inject internal data class InboundGroupSessionHolder( - val wrapper: OlmInboundGroupSessionWrapper2, + val wrapper: MXInboundMegolmSessionWrapper, val mutex: Mutex = Mutex() ) @@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) - oldValue.wrapper.olmInboundGroupSession?.releaseSession() + oldValue.wrapper.session.releaseSession() } } } @@ -67,7 +67,7 @@ internal class InboundGroupSessionStore @Inject constructor( private val timer = Timer() private var timerTask: TimerTask? = null - private val dirtySession = mutableListOf() + private val dirtySession = mutableListOf() @Synchronized fun clear() { @@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor( @Synchronized fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - dirtySession.remove(old.wrapper) + dirtySession.remove(old) store.removeInboundGroupSession(sessionId, senderKey) sessionCache.remove(CacheKey(sessionId, senderKey)) // release removed session - old.wrapper.olmInboundGroupSession?.releaseSession() + old.wrapper.session.releaseSession() internalStoreGroupSession(new, sessionId, senderKey) } @@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor( private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") // We want to batch this a bit for performances - dirtySession.add(holder.wrapper) + dirtySession.add(holder) if (sessionCache[CacheKey(sessionId, senderKey)] == null) { // first time seen, put it in memory cache while waiting for batch insert @@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor( @Synchronized private fun batchSave() { - val toSave = mutableListOf().apply { addAll(dirtySession) } + val toSave = mutableListOf().apply { addAll(dirtySession) } dirtySession.clear() cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") tryOrNull { - store.storeInboundGroupSessions(toSave) + store.storeInboundGroupSessions(toSave.map { it.wrapper }) } } } 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 24b6fd166f..c4a6488258 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 @@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.di.MoshiProvider @@ -38,6 +39,7 @@ import org.matrix.android.sdk.internal.util.convertToUTF8 import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException +import org.matrix.olm.OlmInboundGroupSession import org.matrix.olm.OlmMessage import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmSession @@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor( return MXOutboundSessionInfo( sessionId = sessionId, sharedWithHelper = SharedWithHelper(roomId, sessionId, store), - clock, - restoredOutboundGroupSession.creationTime + clock = clock, + creationTime = restoredOutboundGroupSession.creationTime, + sharedHistory = restoredOutboundGroupSession.sharedHistory ) } return null @@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor( * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. * @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 * @return true if the operation succeeds. */ - fun addInboundGroupSession( - sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean - ): AddSessionResult { - val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean, + sharedHistory: Boolean): AddSessionResult { + val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { + if (exportFormat) { + OlmInboundGroupSession.importSession(sessionKey) + } else { + OlmInboundGroupSession(sessionKey) + } + } + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper // If we have an existing one we should check if the new one is not better if (existingSession != null) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") try { - val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { + 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.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() // Probably should discard it? } - val newKnownFirstIndex = candidateSession.firstKnownIndex + 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.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() return AddSessionResult.NotImported } } @@ -639,36 +649,42 @@ internal class MXOlmDevice @Inject constructor( Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") // sanity check on the new session - val candidateOlmInboundSession = candidateSession.olmInboundGroupSession - if (null == candidateOlmInboundSession) { + if (null == candidateSession) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") return AddSessionResult.NotImported } try { - if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { + if (candidateSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundSession.releaseSession() + candidateSession.releaseSession() return AddSessionResult.NotImported } } catch (e: Throwable) { - candidateOlmInboundSession.releaseSession() + candidateSession.releaseSession() Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") return AddSessionResult.NotImported } - candidateSession.senderKey = senderKey - candidateSession.roomId = roomId - candidateSession.keysClaimed = keysClaimed - candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + val candidateSessionData = InboundGroupSessionData( + senderKey = senderKey, + roomId = roomId, + keysClaimed = keysClaimed, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + sharedHistory = sharedHistory, + ) + val wrapper = MXInboundMegolmSessionWrapper( + candidateSession, + candidateSessionData + ) if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey) } else { - inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey) } - return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) + return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) } /** @@ -677,41 +693,22 @@ internal class MXOlmDevice @Inject constructor( * @param megolmSessionsData the megolm sessions data * @return the successfully imported sessions. */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) for (megolmSessionData in megolmSessionsData) { val sessionId = megolmSessionData.sessionId ?: continue val senderKey = megolmSessionData.senderKey ?: continue val roomId = megolmSessionData.roomId - var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null - - try { - candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - } - - // sanity check - if (candidateSessionToImport?.olmInboundGroupSession == null) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session") - continue - } - - val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession - try { - if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundGroupSession?.releaseSession() - continue - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed") - candidateOlmInboundGroupSession?.releaseSession() + val candidateSessionToImport = try { + MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") continue } + val candidateOlmInboundGroupSession = candidateSessionToImport.session val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper @@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor( sessions.add(candidateSessionToImport) } else { Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } - val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } + val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } + val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex } if (existingFirstKnown == null || candidateFirstKnownIndex == null) { // should not happen? - candidateSessionToImport.olmInboundGroupSession?.releaseSession() + candidateSessionToImport.session.releaseSession() Timber.tag(loggerTag.value) .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") } else { - if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { + if (existingFirstKnown <= candidateFirstKnownIndex) { // Ignore this, keep existing candidateOlmInboundGroupSession.releaseSession() } else { @@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor( ): OlmDecryptionResult { val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) val wrapper = sessionHolder.wrapper - val inboundGroupSession = wrapper.olmInboundGroupSession - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") + val inboundGroupSession = wrapper.session if (roomId != wrapper.roomId) { // Check that the room id matches the original one for the session. This stops // the HS pretending a message was targeting a different room. @@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor( return OlmDecryptionResult( payload, - wrapper.keysClaimed, + wrapper.sessionData.keysClaimed, senderKey, - wrapper.forwardingCurve25519KeyChain + wrapper.sessionData.forwardingCurve25519KeyChain ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt index f6bc9a9148..ca0bdc8a0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt @@ -69,5 +69,13 @@ internal data class MegolmSessionData( * Devices which forwarded this session to us (normally empty). */ @Json(name = "forwarding_curve25519_key_chain") - val forwardingCurve25519KeyChain: List? = null + val forwardingCurve25519KeyChain: List? = null, + + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. + */ + // When this feature lands in spec name = shared_history should be used + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt index 6b22cc09d6..810699d933 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt @@ -437,7 +437,10 @@ internal class OutgoingKeyRequestManager @Inject constructor( if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { // let's see what's the index val knownIndex = tryOrNull { - inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "") + ?.wrapper + ?.session + ?.firstKnownIndex } if (knownIndex != null && knownIndex <= request.fromIndex) { // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index f6ab96aee6..a624b92a19 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -84,8 +84,9 @@ internal class MegolmSessionDataImporter @Inject constructor( megolmSessionData.senderKey ?: "", tryOrNull { olmInboundGroupSessionWrappers - .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } - ?.firstKnownIndex?.toInt() + .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId } + ?.session?.firstKnownIndex + ?.toInt() } ?: 0 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt index 73ce5a5004..1454f5b486 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt @@ -16,7 +16,9 @@ package org.matrix.android.sdk.internal.crypto.algorithms +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder /** * An interface for encrypting data. @@ -32,4 +34,6 @@ internal interface IMXEncrypting { * @return the encrypted content */ suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content + + suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {} } 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 141d6f74cd..410b74e19f 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,6 +17,7 @@ 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.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener @@ -41,6 +42,7 @@ internal class MXMegolmDecryption( private val olmDevice: MXOlmDevice, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, + private val matrixConfiguration: MatrixConfiguration, private val liveEventManager: Lazy ) : IMXDecrypting { @@ -240,13 +242,14 @@ internal class MXMegolmDecryption( Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") val addSessionResult = olmDevice.addInboundGroupSession( - roomKeyContent.sessionId, - roomKeyContent.sessionKey, - roomKeyContent.roomId, - senderKey, - forwardingCurve25519KeyChain, - keysClaimed, - exportFormat + sessionId = roomKeyContent.sessionId, + sessionKey = roomKeyContent.sessionKey, + roomId = roomKeyContent.roomId, + senderKey = senderKey, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + keysClaimed = keysClaimed, + exportFormat = exportFormat, + sharedHistory = roomKeyContent.getSharedKey() ) when (addSessionResult) { @@ -296,6 +299,14 @@ internal class MXMegolmDecryption( } } + /** + * Returns boolean shared key flag, if enabled with respect to matrix configuration. + */ + private fun RoomKeyContent.getSharedKey(): Boolean { + if (!cryptoStore.isShareKeysOnInviteEnabled()) return false + return sharedHistory ?: false + } + /** * Check if the some messages can be decrypted with a new session. * 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 81a6fb28c0..414416a0f6 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,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy +import org.matrix.android.sdk.api.MatrixConfiguration 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 @@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( private val olmDevice: MXOlmDevice, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, + private val matrixConfiguration: MatrixConfiguration, private val eventsManager: Lazy ) { @@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( olmDevice, outgoingKeyRequestManager, cryptoStore, - eventsManager - ) + matrixConfiguration, + eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 7bfbae6edf..48a25f2a8b 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 @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter @@ -151,14 +152,27 @@ internal class MXMegolmEncryption( "ed25519" to olmDevice.deviceEd25519Key!! ) + val sharedHistory = cryptoStore.shouldShareHistory(roomId) + Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory") olmDevice.addInboundGroupSession( - sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, - emptyList(), keysClaimedMap, false + sessionId = sessionId!!, + sessionKey = olmDevice.getSessionKey(sessionId)!!, + roomId = roomId, + senderKey = olmDevice.deviceCurve25519Key!!, + forwardingCurve25519KeyChain = emptyList(), + keysClaimed = keysClaimedMap, + exportFormat = false, + sharedHistory = sharedHistory ) defaultKeysBackupService.maybeBackupKeys() - return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock) + return MXOutboundSessionInfo( + sessionId = sessionId, + sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore), + clock = clock, + sharedHistory = sharedHistory + ) } /** @@ -172,6 +186,8 @@ internal class MXMegolmEncryption( if (session == null || // Need to make a brand new session? session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || + // Is there a room history visibility change since the last outboundSession + cryptoStore.shouldShareHistory(roomId) != session.sharedHistory || // Determine if we have shared with anyone we shouldn't have session.sharedWithTooManyDevices(devicesInRoom)) { Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") @@ -231,26 +247,27 @@ internal class MXMegolmEncryption( /** * Share the device keys of a an user. * - * @param session the session info + * @param sessionInfo the session info * @param devicesByUser the devices map */ - private suspend fun shareUserDevicesKey( - session: MXOutboundSessionInfo, - devicesByUser: Map> - ) { - val sessionKey = olmDevice.getSessionKey(session.sessionId) - val chainIndex = olmDevice.getMessageIndex(session.sessionId) + private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo, + devicesByUser: Map>) { + val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { + Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") + } + val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId) - val submap = HashMap() - submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM - submap["room_id"] = roomId - submap["session_id"] = session.sessionId - submap["session_key"] = sessionKey!! - submap["chain_index"] = chainIndex - - val payload = HashMap() - payload["type"] = EventType.ROOM_KEY - payload["content"] = submap + val payload = mapOf( + "type" to EventType.ROOM_KEY, + "content" to mapOf( + "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM, + "room_id" to roomId, + "session_id" to sessionInfo.sessionId, + "session_key" to sessionKey, + "chain_index" to chainIndex, + "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory + ) + ) var t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") @@ -292,7 +309,7 @@ internal class MXMegolmEncryption( // for dead devices on every message. for ((_, devicesToShareWith) in devicesByUser) { for (deviceInfo in devicesToShareWith) { - session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) + sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) // XXX is it needed to add it to the audit trail? // For now decided that no, we are more interested by forward trail } @@ -300,8 +317,8 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = clock.epochMillis() - Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") - Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target") + Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { withContext(coroutineDispatchers.io) { @@ -310,7 +327,7 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>") + Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>") } } else { Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") @@ -320,7 +337,7 @@ internal class MXMegolmEncryption( // XXX offload?, as they won't read the message anyhow? notifyKeyWithHeld( noOlmToNotify, - session.sessionId, + sessionInfo.sessionId, olmDevice.deviceCurve25519Key, WithHeldCode.NO_OLM ) @@ -514,6 +531,51 @@ internal class MXMegolmEncryption( } } + @Throws + override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) { + if (!inboundSessionWrapper.wrapper.sessionData.sharedHistory) throw IllegalArgumentException("This key can't be shared") + Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") + val userId = deviceInfo.userId + val deviceId = deviceInfo.deviceId + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm") + // process anyway? + null + } + val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys") + return + } + + val export = inboundSessionWrapper.mutex.withLock { + inboundSessionWrapper.wrapper.exportKeys() + } ?: return Unit.also { + Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}") + } + + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) + + val encodedPayload = + withContext(coroutineDispatchers.computation) { + messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + } + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.tag(loggerTag.value) + .d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(sendToDeviceParams) + } + } + data class DeviceInRoomInfo( val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 28d925d8fd..e0caa0d9a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo( private val clock: Clock, // When the session was created private val creationTime: Long = clock.epochMillis(), + val sharedHistory: Boolean = false ) { // Number of times this session has been used 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 5eaa106af3..49cf60d051 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 @@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP @@ -50,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.ObjectSigner @@ -71,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.di.MoshiProvider @@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor( private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor private val taskExecutor: TaskExecutor, + private val matrixConfiguration: MatrixConfiguration, + private val inboundGroupSessionStore: InboundGroupSessionStore, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : KeysBackupService { @@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor( olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach - val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach + val olmInboundGroupSession = olmInboundGroupSessionWrapper.session try { encryptGroupSession(olmInboundGroupSessionWrapper) @@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor( @VisibleForTesting @WorkerThread - fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? { + suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? { + olmInboundGroupSessionWrapper.safeSessionId ?: return null + olmInboundGroupSessionWrapper.senderKey ?: return null // Gather information for each key - val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) } + val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey) // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format - val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null + val sessionData = inboundGroupSessionStore + .getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey) + ?.let { + withContext(coroutineDispatchers.computation) { + it.mutex.withLock { it.wrapper.exportKeys() } + } + } + ?: return null val sessionBackupData = mapOf( "algorithm" to sessionData.algorithm, "sender_key" to sessionData.senderKey, "sender_claimed_keys" to sessionData.senderClaimedKeys, "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), - "session_key" to sessionData.sessionKey + "session_key" to sessionData.sessionKey, + "org.matrix.msc3061.shared_history" to sessionData.sharedHistory ) val json = MoshiProvider.providesMoshi() @@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor( .toJson(sessionBackupData) val encryptedSessionBackupData = try { - backupOlmPkEncryption?.encrypt(json) + withContext(coroutineDispatchers.computation) { + backupOlmPkEncryption?.encrypt(json) + } } catch (e: OlmException) { Timber.e(e, "OlmException") null @@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor( // Build backup data for that key return KeyBackupData( firstMessageIndex = try { - olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0 + olmInboundGroupSessionWrapper.session.firstKnownIndex } catch (e: OlmException) { Timber.e(e, "OlmException") 0L }, - forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size, + forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size, isVerified = device?.isVerified == true, - + sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(), sessionData = mapOf( "ciphertext" to encryptedSessionBackupData.mCipherText, "mac" to encryptedSessionBackupData.mMac, @@ -1451,6 +1468,14 @@ internal class DefaultKeysBackupService @Inject constructor( ) } + /** + * Returns boolean shared key flag, if enabled with respect to matrix configuration. + */ + private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean { + if (!cryptoStore.isShareKeysOnInviteEnabled()) return false + return sessionData.sharedHistory + } + @VisibleForTesting @WorkerThread fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt index 5c3d0c12b0..1817b18e2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -50,5 +50,12 @@ internal data class KeyBackupData( * Algorithm-dependent data. */ @Json(name = "session_data") - val sessionData: JsonDict + val sessionData: JsonDict, + + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean = false ) 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 new file mode 100644 index 0000000000..2ce36aa209 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt @@ -0,0 +1,51 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InboundGroupSessionData( + + /** The room in which this session is used. */ + @Json(name = "room_id") + var roomId: String? = null, + + /** The base64-encoded curve25519 key of the sender. */ + @Json(name = "sender_key") + var senderKey: String? = null, + + /** Other keys the sender claims. */ + @Json(name = "keys_claimed") + var keysClaimed: Map? = null, + + /** Devices which forwarded this session to us (normally emty). */ + @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. + */ + @Json(name = "shared_history") + val sharedHistory: Boolean = false, + + ) 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 new file mode 100644 index 0000000000..2772b34835 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt @@ -0,0 +1,97 @@ +/* + * 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.model + +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber + +data class MXInboundMegolmSessionWrapper( + // olm object + val session: OlmInboundGroupSession, + // data about the session + val sessionData: InboundGroupSessionData +) { + // shortcut + val roomId = sessionData.roomId + val senderKey = sessionData.senderKey + val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() } + + /** + * Export the inbound group session keys. + * @param index the index to export. If null, the first known index will be used + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + internal fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + val keysClaimed = sessionData.keysClaimed ?: return null + val wantedIndex = index ?: session.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(), + sessionKey = session.export(wantedIndex), + senderClaimedKeys = keysClaimed, + roomId = sessionData.roomId, + sessionId = session.sessionIdentifier(), + senderKey = senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sharedHistory = sessionData.sharedHistory + ) + } catch (e: Exception) { + Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed") + null + } + } + + companion object { + + /** + * @exportFormat true if the megolm keys are in export format + * (ie, they lack an ed25519 signature) + */ + @Throws + internal fun newFromMegolmData(megolmSessionData: MegolmSessionData, exportFormat: Boolean): MXInboundMegolmSessionWrapper { + val exportedKey = megolmSessionData.sessionKey ?: throw IllegalArgumentException("key data not found") + val inboundSession = if (exportFormat) { + OlmInboundGroupSession.importSession(exportedKey) + } else { + OlmInboundGroupSession(exportedKey) + } + .also { + if (it.sessionIdentifier() != megolmSessionData.sessionId) { + it.releaseSession() + throw IllegalStateException("Mismatched group session Id") + } + } + val data = InboundGroupSessionData( + roomId = megolmSessionData.roomId, + senderKey = megolmSessionData.senderKey, + keysClaimed = megolmSessionData.senderClaimedKeys, + forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, + sharedHistory = megolmSessionData.sharedHistory, + ) + + return MXInboundMegolmSessionWrapper( + inboundSession, + data + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt index 289c169d6d..600fcb1003 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt @@ -26,6 +26,8 @@ import java.io.Serializable * This class adds more context to a OlmInboundGroupSession object. * This allows additional checks. The class implements Serializable so that the context can be stored. */ +// Note used anymore, just for database migration +// Deprecated("Use MXInboundMegolmSessionWrapper") internal class OlmInboundGroupSessionWrapper2 : Serializable { // The associated olm inbound group session. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt index 4ac87f44ce..5a6d1f4bc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt @@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession internal data class OutboundGroupSessionWrapper( val outboundGroupSession: OlmOutboundGroupSession, - val creationTime: Long + val creationTime: Long, + /** + * As per MSC 3061, declares if this key could be shared when inviting a new user to the room. + */ + val sharedHistory: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt new file mode 100644 index 0000000000..b3a2ba4dfe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt @@ -0,0 +1,22 @@ +/* + * 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.model + +data class SessionInfo( + val sessionId: String, + val senderKey: String +) 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 b5b8d8e974..0413fc730c 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 @@ -35,7 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity @@ -64,7 +64,15 @@ internal interface IMXCryptoStore { * * @return the list of all known group sessions, to export them. */ - fun getInboundGroupSessions(): List + fun getInboundGroupSessions(): List + + /** + * Retrieve the known inbound group sessions for the specified room. + * + * @param roomId The roomId that the sessions will be returned + * @return the list of all known group sessions, for the provided roomId + */ + fun getInboundGroupSessions(roomId: String): List /** * @return true to unilaterally blacklist all unverified devices. @@ -90,6 +98,20 @@ internal interface IMXCryptoStore { fun isKeyGossipingEnabled(): Boolean + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun enableShareKeyOnInvite(enable: Boolean) + + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun isShareKeysOnInviteEnabled(): Boolean + /** * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. * @@ -250,6 +272,17 @@ internal interface IMXCryptoStore { fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + fun shouldShareHistory(roomId: String): Boolean + + /** + * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) + * will be shared to new user invites. + * + * @param roomId the room id + * @param shouldShareHistory The boolean flag + */ + fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) + /** * Store a session between the logged-in user and another device. * @@ -290,7 +323,7 @@ internal interface IMXCryptoStore { * * @param sessions the inbound group sessions to store. */ - fun storeInboundGroupSessions(sessions: List) + fun storeInboundGroupSessions(sessions: List) /** * Retrieve an inbound group session. @@ -299,7 +332,17 @@ internal interface IMXCryptoStore { * @param senderKey the base64-encoded curve25519 key of the sender. * @return an inbound group session. */ - fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? + fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? + + /** + * Retrieve an inbound group session, filtering shared history. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param sharedHistory filter inbound session with respect to shared history field + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? /** * Get the current outbound group session for this encrypted room. @@ -333,7 +376,7 @@ internal interface IMXCryptoStore { * * @param olmInboundGroupSessionWrappers the sessions */ - fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) + fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) /** * Retrieve inbound group sessions that are not yet backed up. @@ -341,7 +384,7 @@ internal interface IMXCryptoStore { * @param limit the maximum number of sessions to return. * @return an array of non backed up inbound group sessions. */ - fun inboundGroupSessionsToBackup(limit: Int): List + fun inboundGroupSessionsToBackup(limit: Int): List /** * Number of stored inbound group sessions. 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 75d090153b..170a544457 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 @@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldCo import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -659,12 +659,28 @@ internal class RealmCryptoStore @Inject constructor( ?: false } + override fun shouldShareHistory(roomId: String): Boolean { + if (!isShareKeysOnInviteEnabled()) return false + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory + } + ?: false + } + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers } } + override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room $roomId is $shouldShareHistory") + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory + } + } + override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { var sessionIdentifier: String? = null @@ -729,54 +745,55 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun storeInboundGroupSessions(sessions: List) { + override fun storeInboundGroupSessions(sessions: List) { if (sessions.isEmpty()) { return } doRealmTransaction(realmConfiguration) { realm -> - sessions.forEach { session -> - var sessionIdentifier: String? = null + sessions.forEach { wrapper -> - try { - sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier() + val sessionIdentifier = try { + wrapper.session.sessionIdentifier() } catch (e: OlmException) { Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") + return@forEach } - if (sessionIdentifier != null) { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) +// val shouldShareHistory = session.roomId?.let { roomId -> +// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory +// } ?: false + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - if (existing != null) { - // we want to keep the existing backup status - existing.putInboundGroupSession(session) - } else { - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - senderKey = session.senderKey - putInboundGroupSession(session) - } - - realm.insertOrUpdate(realmOlmInboundGroupSession) - } + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + store(wrapper) } + Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") + realm.insertOrUpdate(realmOlmInboundGroupSession) } } } - override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - return doWithRealm(realmConfiguration) { - it.where() + return doWithRealm(realmConfiguration) { realm -> + realm.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() - ?.getInboundGroupSession() + ?.toModel() + } + } + + override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory) + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.toModel() } } @@ -788,7 +805,8 @@ internal class RealmCryptoStore @Inject constructor( entity.getOutboundGroupSession()?.let { OutboundGroupSessionWrapper( it, - entity.creationTime ?: 0 + entity.creationTime ?: 0, + entity.shouldShareHistory ) } } @@ -808,6 +826,8 @@ internal class RealmCryptoStore @Inject constructor( if (outboundGroupSession != null) { val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { creationTime = clock.epochMillis() + // Store the room history visibility on the outbound session creation + shouldShareHistory = entity.shouldShareHistory putOutboundGroupSession(outboundGroupSession) } entity.outboundSessionInfo = info @@ -816,17 +836,32 @@ internal class RealmCryptoStore @Inject constructor( } } +// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean { +// return doWithRealm(realmConfiguration) { realm -> +// CryptoRoomEntity.getById(realm, roomId)?.let { entity -> +// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory +// } +// } ?: false +// } + /** * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management. */ - override fun getInboundGroupSessions(): List { - return doWithRealm(realmConfiguration) { - it.where() + override fun getInboundGroupSessions(): List { + return doWithRealm(realmConfiguration) { realm -> + realm.where() .findAll() - .mapNotNull { inboundGroupSessionEntity -> - inboundGroupSessionEntity.getInboundGroupSession() - } + .mapNotNull { it.toModel() } + } + } + + override fun getInboundGroupSessions(roomId: String): List { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId) + .findAll() + .mapNotNull { it.toModel() } } } @@ -887,7 +922,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { + override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { if (olmInboundGroupSessionWrappers.isEmpty()) { return } @@ -895,10 +930,13 @@ internal class RealmCryptoStore @Inject constructor( doRealmTransaction(realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { - val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() + val sessionIdentifier = + tryOrNull("Failed to get session identifier") { + olmInboundGroupSessionWrapper.session.sessionIdentifier() + } ?: return@forEach val key = OlmInboundGroupSessionEntity.createPrimaryKey( sessionIdentifier, - olmInboundGroupSessionWrapper.senderKey + olmInboundGroupSessionWrapper.sessionData.senderKey ) val existing = realm.where() @@ -911,9 +949,7 @@ internal class RealmCryptoStore @Inject constructor( // ... might be in cache but not yet persisted, create a record to persist backedup state val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { primaryKey = key - sessionId = sessionIdentifier - senderKey = olmInboundGroupSessionWrapper.senderKey - putInboundGroupSession(olmInboundGroupSessionWrapper) + store(olmInboundGroupSessionWrapper) backedUp = true } @@ -926,15 +962,13 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun inboundGroupSessionsToBackup(limit: Int): List { + override fun inboundGroupSessionsToBackup(limit: Int): List { return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) .limit(limit.toLong()) .findAll() - .mapNotNull { inboundGroupSession -> - inboundGroupSession.getInboundGroupSession() - } + .mapNotNull { it.toModel() } } } @@ -975,6 +1009,18 @@ internal class RealmCryptoStore @Inject constructor( } ?: false } + override fun isShareKeysOnInviteEnabled(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.enableKeyForwardingOnInvite + } ?: false + } + + override fun enableShareKeyOnInvite(enable: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.enableKeyForwardingOnInvite = enable + } + } + override fun setDeviceKeysUploaded(uploaded: Boolean) { doRealmTransaction(realmConfiguration) { it.where().findFirst()?.deviceKeysSentToServer = uploaded 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 02c2a27dec..4ca9d44f98 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,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -51,7 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - val schemaVersion = 16L + val schemaVersion = 17L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -72,5 +73,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 16) MigrateCryptoTo016(realm).perform() + if (oldVersion < 17) MigrateCryptoTo017(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt new file mode 100644 index 0000000000..8904c412cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt @@ -0,0 +1,101 @@ +/* + * 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.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import timber.log.Timber + +/** + * Version 17L enhance OlmInboundGroupSessionEntity to support shared history for MSC3061. + * Also migrates how megolm session are stored to avoid additional serialized frozen class. + */ +internal class MigrateCryptoTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform { + // We don't have access to the session database to check for the state here and set the good value. + // But for now as it's behind a lab flag, will set to false and force initial sync when enabled + it.setBoolean(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, false) + } + + realm.schema.get("OutboundGroupSessionInfoEntity") + ?.addField(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform { + // We don't have access to the session database to check for the state here and set the good value. + // But for now as it's behind a lab flag, will set to false and force initial sync when enabled + it.setBoolean(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, false) + } + + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, Boolean::class.java) + ?.transform { obj -> + // default to false + obj.setBoolean(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, false) + } + + val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java) + + realm.schema.get("OlmInboundGroupSessionEntity") + ?.addField(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, Boolean::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.ROOM_ID, String::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, String::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, String::class.java) + ?.transform { dynamicObject -> + try { + // we want to convert the old wrapper frozen class into a + // map of sessionData & the pickled session herself + dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { oldData -> + val oldWrapper = tryOrNull("Failed to convert megolm inbound group data") { + @Suppress("DEPRECATION") + deserializeFromRealm(oldData) + } + val groupSession = oldWrapper?.olmInboundGroupSession + ?: return@transform Unit.also { + Timber.w("Failed to migrate megolm session, no olmInboundGroupSession") + } + // now convert to new data + val data = InboundGroupSessionData( + senderKey = oldWrapper.senderKey, + roomId = oldWrapper.roomId, + keysClaimed = oldWrapper.keysClaimed, + forwardingCurve25519KeyChain = oldWrapper.forwardingCurve25519KeyChain, + sharedHistory = false, + ) + + dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(data)) + dynamicObject.setString(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, serializeForRealm(groupSession)) + + // denormalized fields + dynamicObject.setString(OlmInboundGroupSessionEntityFields.ROOM_ID, oldWrapper.roomId) + dynamicObject.setBoolean(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, false) + } + } 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/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 63ed0e537e..88708f824e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -35,6 +35,11 @@ internal open class CryptoMetadataEntity( var globalBlacklistUnverifiedDevices: Boolean = false, // setting to enable or disable key gossiping var globalEnableKeyGossiping: Boolean = true, + + // MSC3061: Sharing room keys for past messages + // If set to true key history will be shared to invited users with respect to room setting + var enableKeyForwardingOnInvite: Boolean = false, + // The keys backup version currently used. Null means no backup. var backupVersion: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt index 114a596964..be57586163 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -24,6 +24,8 @@ internal open class CryptoRoomEntity( var algorithm: String? = null, var shouldEncryptForInvitedMembers: Boolean? = null, var blacklistUnverifiedDevices: Boolean = false, + // Determines whether or not room history should be shared on new member invites + var shouldShareHistory: Boolean = false, // Store the current outbound session for this room, // to avoid re-create and re-share at each startup (if rotation not needed..) // This is specific to megolm but not sure how to model it better diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index a4f6c279ac..62ab73e379 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.model import io.realm.RealmObject import io.realm.annotations.PrimaryKey -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.olm.OlmInboundGroupSession import timber.log.Timber internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" @@ -28,27 +31,83 @@ internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: internal open class OlmInboundGroupSessionEntity( // Combined value to build a primary key @PrimaryKey var primaryKey: String? = null, + + // denormalization for faster querying (these fields are in the inboundGroupSessionDataJson) var sessionId: String? = null, var senderKey: String? = null, - // olmInboundGroupSessionData contains Json + var roomId: String? = null, + + // Deprecated, used for migration / olmInboundGroupSessionData contains Json + // keep it in case of problem to have a chance to recover var olmInboundGroupSessionData: String? = null, + + // Stores the session data in an extensible format + // to allow to store data not yet supported for later use + var inboundGroupSessionDataJson: String? = null, + + // The pickled session + var serializedOlmInboundGroupSession: String? = null, + + // Flag that indicates whether or not the current inboundSession will be shared to + // invited users to decrypt past messages + var sharedHistory: Boolean = false, // Indicate if the key has been backed up to the homeserver var backedUp: Boolean = false ) : RealmObject() { - fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { + fun store(wrapper: MXInboundMegolmSessionWrapper) { + this.serializedOlmInboundGroupSession = serializeForRealm(wrapper.session) + this.inboundGroupSessionDataJson = adapter.toJson(wrapper.sessionData) + this.roomId = wrapper.sessionData.roomId + this.senderKey = wrapper.sessionData.senderKey + this.sessionId = wrapper.session.sessionIdentifier() + this.sharedHistory = wrapper.sessionData.sharedHistory + } +// fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { +// return try { +// deserializeFromRealm(olmInboundGroupSessionData) +// } catch (failure: Throwable) { +// Timber.e(failure, "## Deserialization failure") +// return null +// } +// } +// +// fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { +// olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) +// } + + fun getOlmGroupSession(): OlmInboundGroupSession? { return try { - deserializeFromRealm(olmInboundGroupSessionData) + deserializeFromRealm(serializedOlmInboundGroupSession) } catch (failure: Throwable) { Timber.e(failure, "## Deserialization failure") return null } } - fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { - olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) + fun getData(): InboundGroupSessionData? { + return try { + inboundGroupSessionDataJson?.let { + adapter.fromJson(it) + } + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } } - companion object + fun toModel(): MXInboundMegolmSessionWrapper? { + val data = getData() ?: return null + val session = getOlmGroupSession() ?: return null + return MXInboundMegolmSessionWrapper( + session = session, + sessionData = data + ) + } + + companion object { + private val adapter = MoshiProvider.providesMoshi() + .adapter(InboundGroupSessionData::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt index d50db78415..2ebd550201 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt @@ -24,7 +24,8 @@ import timber.log.Timber internal open class OutboundGroupSessionInfoEntity( var serializedOutboundSessionData: String? = null, - var creationTime: Long? = null + var creationTime: Long? = null, + var shouldShareHistory: Boolean = false ) : RealmObject() { fun getOutboundGroupSession(): OlmOutboundGroupSession? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index fbd9d245d9..bb14b417dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor( params.event.roomId ?.takeIf { params.encrypt } ?.let { roomId -> - tryOrNull { + try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } catch (failure: Throwable) { + // send any way? + // the result is that some users won't probably be able to decrypt :/ + Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 2263cdf7e6..c81cc72b31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -19,7 +19,11 @@ package org.matrix.android.sdk.internal.database.helper import de.spiritcroc.matrixsdk.util.Dimber import io.realm.Realm import io.realm.kotlin.createObject +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.crypto.model.SessionInfo +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity @@ -32,6 +36,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection @@ -207,3 +212,12 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity, dimber: Dim // We don't know, so we assume it's false return false.also { dimber?.i { "isMoreReacentThan = false (fallback)" } } } + +internal fun ChunkEntity.Companion.findLatestSessionInfo(realm: Realm, roomId: String): Set? = + ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.mapNotNull { timelineEvent -> + timelineEvent?.root?.asDomain()?.content?.toModel()?.let { content -> + content.sessionId ?: return@mapNotNull null + content.senderKey ?: return@mapNotNull null + SessionInfo(content.sessionId, content.senderKey) + } + }?.toSet() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt index e0dbf2eee8..e17d07c584 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt @@ -23,13 +23,20 @@ import io.realm.kotlin.createObject import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +internal fun CurrentStateEventEntity.Companion.whereRoomId( + realm: Realm, + roomId: String +): RealmQuery { + return realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) +} + internal fun CurrentStateEventEntity.Companion.whereType( realm: Realm, roomId: String, type: String ): RealmQuery { - return realm.where(CurrentStateEventEntity::class.java) - .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + return whereRoomId(realm = realm, roomId = roomId) .equalTo(CurrentStateEventEntityFields.TYPE, type) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index b5b46a3f5a..113e780e5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import okhttp3.ConnectionSpec +import okhttp3.Dispatcher import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.logging.HttpLoggingInterceptor @@ -73,7 +74,9 @@ internal object NetworkModule { apiInterceptor: ApiInterceptor ): OkHttpClient { val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build() - + val dispatcher = Dispatcher().apply { + maxRequestsPerHost = 20 + } return OkHttpClient.Builder() // workaround for #4669 .protocols(listOf(Protocol.HTTP_1_1)) @@ -94,6 +97,7 @@ internal object NetworkModule { addInterceptor(curlLoggingInterceptor) } } + .dispatcher(dispatcher) .connectionSpecs(Collections.singletonList(spec)) .applyMatrixConfiguration(matrixConfiguration) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt index 00cbe0aa85..c6ea2bc7bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt @@ -20,6 +20,7 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.RealmObjectSchema import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.util.fatalError internal fun RealmObject.assertIsManaged() { check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" } @@ -27,10 +28,19 @@ internal fun RealmObject.assertIsManaged() { /** * Clear a RealmList by deleting all its items calling the provided lambda. + * The lambda is supposed to delete the item, which means that after this operation, the list will be empty. */ internal fun RealmList.clearWith(delete: (T) -> Unit) { - while (!isEmpty()) { - first()?.let { delete.invoke(it) } + map { item -> + // Create a lambda for all items of the list + { delete(item) } + }.forEach { lambda -> + // Then invoke all the lambda + lambda.invoke() + } + + if (isNotEmpty()) { + fatalError("`clearWith` MUST delete all elements of the RealmList") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt index 56d9cc2143..7d52d9b2bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt @@ -20,6 +20,7 @@ import android.content.Context import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.DiscoveryInformation import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -145,7 +146,8 @@ internal class DefaultLegacySessionImporter @Inject constructor( forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions() ), // If token is not valid, this boolean will be updated later - isTokenValid = true + isTokenValid = true, + loginType = LoginType.UNKNOWN, ) Timber.d("Migration: save session") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt index f75fb01746..90d2719e25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt @@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con override fun register(hasChanged: () -> Unit) { hasChangedCallback = hasChanged - conn.registerDefaultNetworkCallback(networkCallback) + // Add a try catch for safety + // XXX: It happens when running all tests in CI, at some points we reach a limit here causing TooManyRequestsException + // and crashing the sync thread. We might have problem here, would need some investigation + // for now adding a catch to allow CI to continue running + try { + conn.registerDefaultNetworkCallback(networkCallback) + } catch (t: Throwable) { + Timber.e(t, "Unable to register default network callback") + } } override fun unregister() { 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 5e6d052443..6fd4f752a8 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 @@ -43,7 +43,9 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask @@ -60,6 +62,8 @@ import javax.inject.Inject internal class DefaultRoomService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, + private val createLocalRoomTask: CreateLocalRoomTask, + private val deleteLocalRoomTask: DeleteLocalRoomTask, private val joinRoomTask: JoinRoomTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, @@ -78,6 +82,14 @@ internal class DefaultRoomService @Inject constructor( return createRoomTask.executeRetry(createRoomParams, 3) } + override suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String { + return createLocalRoomTask.execute(createRoomParams) + } + + override suspend fun deleteLocalRoom(roomId: String) { + deleteLocalRoomTask.execute(DeleteLocalRoomTask.Params(roomId)) + } + override fun getRoom(roomId: String): Room? { return roomGetter.getRoom(roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index ac2880de69..9bcb7b8e4c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -377,7 +377,7 @@ internal interface RoomAPI { * Get a list of aliases maintained by the local server for the given room. * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-aliases */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases") + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/aliases") suspend fun getAliases(@Path("roomId") roomId: String): GetAliasesResponse /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt new file mode 100644 index 0000000000..bcbc53f95e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt @@ -0,0 +1,55 @@ +/* + * 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.session.room + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import javax.inject.Inject + +internal class RoomDataSource @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) { + fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType { + var result: RoomMembersLoadStatusType? + Realm.getInstance(monarchy.realmConfiguration).use { + result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus + } + return result ?: RoomMembersLoadStatusType.NONE + } + + fun getRoomMembersLoadStatusLive(roomId: String): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { + RoomEntity.where(it, roomId) + }, + { + it.membersLoadStatus == RoomMembersLoadStatusType.LOADED + } + ) + + return Transformations.map(liveData) { results -> + results.firstOrNull().orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 1306c788f9..70a8860fae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -43,8 +43,12 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAli import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask +import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask +import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask +import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask @@ -206,6 +210,12 @@ internal abstract class RoomModule { @Binds abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask + @Binds + abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask + + @Binds + abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask + @Binds abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt index 1ff4156ed3..62681c89d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt @@ -34,7 +34,6 @@ internal class DefaultGetRoomLocalAliasesTask @Inject constructor( ) : GetRoomLocalAliasesTask { override suspend fun execute(params: GetRoomLocalAliasesTask.Params): List { - // We do not check for "org.matrix.msc2432", so the API may be missing val response = executeRequest(globalErrorReceiver) { roomAPI.getAliases(roomId = params.roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt new file mode 100644 index 0000000000..d57491a4c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -0,0 +1,267 @@ +/* + * 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.session.room.create + +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.createObject +import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType +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.getOrNull +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface CreateLocalRoomTask : Task + +internal class DefaultCreateLocalRoomTask @Inject constructor( + @UserId private val userId: String, + @SessionDatabase private val monarchy: Monarchy, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val roomSummaryUpdater: RoomSummaryUpdater, + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val createRoomBodyBuilder: CreateRoomBodyBuilder, + private val userService: UserService, + private val clock: Clock, +) : CreateLocalRoomTask { + + override suspend fun execute(params: CreateRoomParams): String { + val createRoomBody = createRoomBodyBuilder.build(params.withDefault()) + val roomId = RoomLocalEcho.createLocalEchoId() + monarchy.awaitTransaction { realm -> + createLocalRoomEntity(realm, roomId, createRoomBody) + createLocalRoomSummaryEntity(realm, roomId, createRoomBody) + } + + // Wait for room to be created in DB + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + } catch (exception: TimeoutCancellationException) { + throw CreateRoomFailure.CreatedWithTimeout(roomId) + } + + return roomId + } + + /** + * Create a local room entity from the given room creation params. + * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room. + */ + private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { + RoomEntity.getOrCreate(realm, roomId).apply { + membership = Membership.JOIN + chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody)) + membersLoadStatus = RoomMembersLoadStatusType.LOADED + } + } + + private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { + val otherUserId = createRoomBody.getDirectUserId() + if (otherUserId != null) { + RoomSummaryEntity.getOrCreate(realm, roomId).apply { + isDirect = true + directUserId = otherUserId + } + } + roomSummaryUpdater.update( + realm = realm, + roomId = roomId, + membership = Membership.JOIN, + roomSummary = RoomSyncSummary( + heroes = createRoomBody.invitedUserIds.orEmpty().take(5), + joinedMembersCount = 1, + invitedMembersCount = createRoomBody.invitedUserIds?.size ?: 0 + ), + updateMembers = !createRoomBody.invitedUserIds.isNullOrEmpty() + ) + } + + /** + * Create a single chunk containing the necessary events to display the local room. + * + * @param realm the current instance of realm + * @param roomId the id of the local room + * @param createRoomBody the room creation params + * + * @return a chunk entity + */ + private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { + val chunkEntity = realm.createObject().apply { + isLastBackward = true + isLastForward = true + } + + val eventList = createLocalRoomEvents(createRoomBody) + val roomMemberContentsByUser = HashMap() + + for (event in eventList) { + if (event.eventId == null || event.senderId == null || event.type == null) { + continue + } + + val now = clock.epochMillis() + val eventEntity = event.toEntity(roomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent() + roomMemberEventHandler.handle(realm, roomId, event, false) + } + } + + roomMemberContentsByUser.getOrPut(event.senderId) { + // If we don't have any new state on this user, get it from db + val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root + rootStateEvent?.asDomain()?.getFixedRoomMemberContent() + } + + chunkEntity.addTimelineEvent( + roomId = roomId, + eventEntity = eventEntity, + direction = PaginationDirection.FORWARDS, + roomMemberContentsByUser = roomMemberContentsByUser + ) + } + + return chunkEntity + } + + /** + * Build the list of the events related to the room creation params. + * + * @param createRoomBody the room creation params + * + * @return the list of events + */ + private suspend fun createLocalRoomEvents(createRoomBody: CreateRoomBody): List { + val myUser = userService.getUser(userId) ?: User(userId) + val invitedUsers = createRoomBody.invitedUserIds.orEmpty() + .mapNotNull { tryOrNull { userService.resolveUser(it) } } + + val createRoomEvent = createLocalEvent( + type = EventType.STATE_ROOM_CREATE, + content = RoomCreateContent( + creator = userId + ).toContent() + ) + val myRoomMemberEvent = createLocalEvent( + type = EventType.STATE_ROOM_MEMBER, + content = RoomMemberContent( + membership = Membership.JOIN, + displayName = myUser.displayName, + avatarUrl = myUser.avatarUrl + ).toContent(), + stateKey = userId + ) + val roomMemberEvents = invitedUsers.map { + createLocalEvent( + type = EventType.STATE_ROOM_MEMBER, + content = RoomMemberContent( + isDirect = createRoomBody.isDirect.orFalse(), + membership = Membership.INVITE, + displayName = it.displayName, + avatarUrl = it.avatarUrl + ).toContent(), + stateKey = it.userId + ) + } + + return buildList { + add(createRoomEvent) + add(myRoomMemberEvent) + addAll(createRoomBody.initialStates.orEmpty().map { createLocalEvent(it.type, it.content, it.stateKey) }) + addAll(roomMemberEvents) + } + } + + /** + * Generate a local event from the given parameters. + * + * @param type the event type, see [EventType] + * @param content the content of the Event + * @param stateKey the stateKey, if any + * + * @return a fake event + */ + private fun createLocalEvent(type: String?, content: Content?, stateKey: String? = ""): Event { + return Event( + type = type, + senderId = userId, + stateKey = stateKey, + content = content, + originServerTs = clock.epochMillis(), + eventId = LocalEcho.createLocalEchoId() + ) + } + + /** + * Setup default values to the CreateRoomParams as the room is created locally (the default values will not be defined by the server). + */ + private fun CreateRoomParams.withDefault() = this.apply { + if (visibility == null) visibility = RoomDirectoryVisibility.PRIVATE + if (historyVisibility == null) historyVisibility = RoomHistoryVisibility.SHARED + if (guestAccess == null) guestAccess = GuestAccess.Forbidden + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt index cffa632768..b326c3618c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -120,3 +120,20 @@ internal data class CreateRoomBody( @Json(name = "room_version") val roomVersion: String? ) + +/** + * Tells if the created room can be a direct chat one. + * + * @return true if it is a direct chat + */ +private fun CreateRoomBody.isDirect(): Boolean { + return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && isDirect == true +} + +internal fun CreateRoomBody.getDirectUserId(): String? { + return if (isDirect()) { + invitedUserIds?.firstOrNull() + ?: invite3pids?.firstOrNull()?.address + ?: throw IllegalStateException("You can't create a direct room without an invitedUser") + } else null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 6dd2c91048..d76640573f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -62,11 +62,6 @@ internal class DefaultCreateRoomTask @Inject constructor( ) : CreateRoomTask { override suspend fun execute(params: CreateRoomParams): String { - val otherUserId = if (params.isDirect()) { - params.getFirstInvitedUserId() - ?: throw IllegalStateException("You can't create a direct room without an invitedUser") - } else null - if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) { try { aliasAvailabilityChecker.check(params.roomAliasName) @@ -111,14 +106,13 @@ internal class DefaultCreateRoomTask @Inject constructor( RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis() } - if (otherUserId != null) { - handleDirectChatCreation(roomId, otherUserId) - } + handleDirectChatCreation(roomId, createRoomBody.getDirectUserId()) setReadMarkers(roomId) return roomId } - private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String) { + private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String?) { + otherUserId ?: return // This is not a direct room monarchy.awaitTransaction { realm -> RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { this.directUserId = otherUserId @@ -133,21 +127,4 @@ internal class DefaultCreateRoomTask @Inject constructor( val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true) return readMarkersTask.execute(setReadMarkerParams) } - - /** - * Tells if the created room can be a direct chat one. - * - * @return true if it is a direct chat - */ - private fun CreateRoomParams.isDirect(): Boolean { - return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && - isDirect == true - } - - /** - * @return the first invited user id - */ - private fun CreateRoomParams.getFirstInvitedUserId(): String? { - return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value - } } 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 new file mode 100644 index 0000000000..936c94e520 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt @@ -0,0 +1,78 @@ +/* + * 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.session.room.delete + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho +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.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.whereRoomId +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import timber.log.Timber +import javax.inject.Inject + +internal interface DeleteLocalRoomTask : Task { + data class Params(val roomId: String) +} + +internal class DefaultDeleteLocalRoomTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : DeleteLocalRoomTask { + + override suspend fun execute(params: Params) { + val roomId = params.roomId + + if (RoomLocalEcho.isLocalEchoId(roomId)) { + monarchy.awaitTransaction { realm -> + Timber.i("## DeleteLocalRoomTask - delete local room id $roomId") + RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + CurrentStateEventEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - CurrentStateEventEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + EventEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - EventEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + TimelineEventEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") } + ?.forEach { it.deleteOnCascade(true) } + ChunkEntity.where(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ChunkEntity - delete ${it.size} entries") } + ?.forEach { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) } + RoomSummaryEntity.where(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + RoomEntity.where(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + } + } else { + Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 20320cad23..a8a9691ce9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -72,7 +72,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return sendLiveLocationTask.execute(params) } - override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { + override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult { // Ensure to stop any active live before starting a new one if (checkIfExistingActiveLive()) { val result = stopLiveLocationShare() @@ -82,7 +82,8 @@ internal class DefaultLocationSharingService @AssistedInject constructor( } val params = StartLiveLocationShareTask.Params( roomId = roomId, - timeoutMillis = timeoutMillis + timeoutMillis = timeoutMillis, + description = description ) return startLiveLocationShareTask.execute(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index b943c27977..79019e4765 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -30,6 +30,7 @@ internal interface StartLiveLocationShareTask : Task { + return roomDataSource.getRoomMembersLoadStatusLive(roomId) + } + override fun getRoomMember(userId: String): RoomMemberSummary? { val roomMemberEntity = monarchy.fetchCopied { RoomMemberHelper(it, roomId).getLastRoomMember(userId) @@ -127,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor( } override suspend fun invite(userId: String, reason: String?) { + sendShareHistoryKeysIfNeeded(userId) val params = InviteTask.Params(roomId, userId, reason) inviteTask.execute(params) } + private suspend fun sendShareHistoryKeysIfNeeded(userId: String) { + if (!cryptoService.isShareKeysOnInviteEnabled()) return + // TODO not sure it's the right way to get the latest messages in a room + val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use { + ChunkEntity.findLatestSessionInfo(it, roomId) + } + cryptoService.sendSharedHistoryKeys(roomId, userId, sessionInfo) + } + override suspend fun invite3pid(threePid: ThreePid) { val params = InviteThreePidTask.Params(roomId, threePid) return inviteThreePidTask.execute(params) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index db029783e9..ac94408cc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.membership import com.zhuinden.monarchy.Monarchy -import io.realm.Realm import io.realm.kotlin.createObject import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.session.room.model.Membership @@ -38,6 +37,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.RoomDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task @@ -58,6 +58,7 @@ internal interface LoadRoomMembersTask : Task internal class DefaultLoadRoomMembersTask @Inject constructor( private val roomAPI: RoomAPI, @SessionDatabase private val monarchy: Monarchy, + private val roomDataSource: RoomDataSource, private val syncTokenStore: SyncTokenStore, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomMemberEventHandler: RoomMemberEventHandler, @@ -68,7 +69,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { - when (getRoomMembersLoadStatus(params.roomId)) { + when (roomDataSource.getRoomMembersLoadStatus(params.roomId)) { RoomMembersLoadStatusType.NONE -> doRequest(params) RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) RoomMembersLoadStatusType.LOADED -> Unit @@ -142,14 +143,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( } } - private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType { - var result: RoomMembersLoadStatusType? - Realm.getInstance(monarchy.realmConfiguration).use { - result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus - } - return result ?: RoomMembersLoadStatusType.NONE - } - private suspend fun setRoomMembersLoadStatus(roomId: String, status: RoomMembersLoadStatusType) { monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index dae25ae4b1..f5774a88ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface +import org.matrix.android.sdk.api.extensions.ensureNotEmpty import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -707,6 +708,7 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") + MessageType.MSGTYPE_BEACON_INFO -> return TextContent(content.body.ensureNotEmpty() ?: "shared live location.") MessageType.MSGTYPE_POLL_START -> { return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt new file mode 100644 index 0000000000..323eee0b1c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt @@ -0,0 +1,32 @@ +/* + * 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.util + +import org.matrix.android.sdk.BuildConfig +import timber.log.Timber + +/** + * Throws in debug, only log in production. + * As this method does not always throw, next statement should be a return. +*/ +internal fun fatalError(message: String) { + if (BuildConfig.DEBUG) { + error(message) + } else { + Timber.e(message) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.kt new file mode 100644 index 0000000000..95b226411b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.kt @@ -0,0 +1,33 @@ +/* + * 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.auth.db.migration + +import org.junit.Test +import org.matrix.android.sdk.test.fakes.internal.auth.db.migration.Fake005MigrationRealm + +class MigrateAuthTo005Test { + + private val fakeRealm = Fake005MigrationRealm() + private val migrator = MigrateAuthTo005(fakeRealm.instance) + + @Test + fun `when doMigrate, then LoginType field added`() { + migrator.doMigrate(fakeRealm.instance) + + fakeRealm.verifyLoginTypeAdded() + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt new file mode 100644 index 0000000000..495302acb2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt @@ -0,0 +1,46 @@ +/* + * 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.auth.login + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.auth.LoginType + +class LoginTypeTest { + + @Test + fun `when getting type fromName, then map correctly`() { + LoginType.fromName(LoginType.PASSWORD.name) shouldBeEqualTo LoginType.PASSWORD + LoginType.fromName(LoginType.SSO.name) shouldBeEqualTo LoginType.SSO + LoginType.fromName(LoginType.UNSUPPORTED.name) shouldBeEqualTo LoginType.UNSUPPORTED + LoginType.fromName(LoginType.CUSTOM.name) shouldBeEqualTo LoginType.CUSTOM + LoginType.fromName(LoginType.DIRECT.name) shouldBeEqualTo LoginType.DIRECT + LoginType.fromName(LoginType.UNKNOWN.name) shouldBeEqualTo LoginType.UNKNOWN + } + + @Test // The failure of this test means that an existing type has not been correctly added to fromValue + fun `given non-unknown type name, when getting type fromName, then type is not UNKNOWN`() { + val types = LoginType.values() + + types.forEach { type -> + if (type != LoginType.UNKNOWN) { + LoginType.fromName(type.name) shouldNotBeEqualTo LoginType.UNKNOWN + } + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index de91206531..ef9bde2c49 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -51,6 +51,7 @@ private const val A_LATITUDE = 1.4 private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L +private const val A_DESCRIPTION = "description" @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { @@ -137,7 +138,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id") coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -150,7 +151,8 @@ internal class DefaultLocationSharingServiceTest { coVerify { stopLiveLocationShareTask.execute(expectedStopParams) } val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, - timeoutMillis = A_TIMEOUT + timeoutMillis = A_TIMEOUT, + description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } @@ -161,7 +163,7 @@ internal class DefaultLocationSharingServiceTest { val error = Throwable() coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -179,7 +181,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -188,7 +190,8 @@ internal class DefaultLocationSharingServiceTest { coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) } val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, - timeoutMillis = A_TIMEOUT + timeoutMillis = A_TIMEOUT, + description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index 909ba5d048..aa8826243f 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask private const val A_USER_ID = "user-id" private const val A_ROOM_ID = "room-id" private const val AN_EVENT_ID = "event-id" +private const val A_DESCRIPTION = "description" private const val A_TIMEOUT = 15_000L private const val AN_EPOCH = 1655210176L @@ -58,7 +59,8 @@ internal class DefaultStartLiveLocationShareTaskTest { fun `given parameters and no error when calling the task then result is success`() = runTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, - timeoutMillis = A_TIMEOUT + timeoutMillis = A_TIMEOUT, + description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) @@ -67,6 +69,7 @@ internal class DefaultStartLiveLocationShareTaskTest { result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedBeaconContent = MessageBeaconInfoContent( + body = A_DESCRIPTION, timeout = params.timeoutMillis, isLive = true, unstableTimestampMillis = AN_EPOCH @@ -87,7 +90,8 @@ internal class DefaultStartLiveLocationShareTaskTest { fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, - timeoutMillis = A_TIMEOUT + timeoutMillis = A_TIMEOUT, + description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns("") @@ -101,7 +105,8 @@ internal class DefaultStartLiveLocationShareTaskTest { fun `given parameters and error during event sending when calling the task then result is failure`() = runTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, - timeoutMillis = A_TIMEOUT + timeoutMillis = A_TIMEOUT, + description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) val error = Throwable() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt new file mode 100644 index 0000000000..df22455fb1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.api + +import io.mockk.mockk +import org.matrix.android.sdk.api.session.Session + +class FakeSession { + + val instance: Session = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt new file mode 100644 index 0000000000..2232ad9b4f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt @@ -0,0 +1,45 @@ +/* + * 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.internal + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.amshove.kluent.shouldBeEqualTo +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.test.fakes.api.FakeSession + +internal class FakeSessionManager { + + val instance: SessionManager = mockk() + + init { + every { instance.getOrCreateSession(any()) } returns fakeSession.instance + } + + fun assertSessionCreatedWithParams(session: Session, sessionParams: SessionParams) { + verify { instance.getOrCreateSession(sessionParams) } + + session shouldBeEqualTo fakeSession.instance + } + + companion object { + private val fakeSession = FakeSession() + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt new file mode 100644 index 0000000000..40681748c1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt @@ -0,0 +1,45 @@ +/* + * 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.internal.auth + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask +import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask.Params + +internal class FakeIsValidClientServerApiTask { + + init { + coEvery { instance.execute(any()) } returns true + } + + val instance: IsValidClientServerApiTask = mockk() + + fun givenValidationFails() { + coEvery { instance.execute(any()) } returns false + } + + fun verifyExecutionWithConfig(config: HomeServerConnectionConfig) { + coVerify { instance.execute(Params(config)) } + } + + fun verifyNoExecution() { + coVerify(inverse = true) { instance.execute(any()) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt new file mode 100644 index 0000000000..8a18b75ca2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt @@ -0,0 +1,35 @@ +/* + * 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.internal.auth + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.internal.auth.PendingSessionStore + +internal class FakePendingSessionStore { + + val instance: PendingSessionStore = mockk() + + init { + coJustRun { instance.delete() } + } + + fun verifyPendingSessionDataCleared() { + coVerify { instance.delete() } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt new file mode 100644 index 0000000000..f64e5a451d --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt @@ -0,0 +1,51 @@ +/* + * 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.internal.auth + +import android.net.Uri +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.SessionParamsCreator +import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams + +internal class FakeSessionParamsCreator { + + val instance: SessionParamsCreator = mockk() + + init { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockk() + coEvery { instance.create(any(), any(), any()) } returns sessionParams + } + + fun verifyCreatedWithParameters( + credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + loginType: LoginType, + ) { + coVerify { instance.create(credentials, homeServerConnectionConfig, loginType) } + } + + companion object { + val sessionParams = aSessionParams() + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.kt new file mode 100644 index 0000000000..22e8a32a32 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.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.internal.auth + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.internal.auth.SessionParamsStore + +internal class FakeSessionParamsStore { + + val instance: SessionParamsStore = mockk() + + init { + coJustRun { instance.save(any()) } + } + + fun verifyParamsSaved(sessionParams: SessionParams) { + coVerify { instance.save(sessionParams) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt new file mode 100644 index 0000000000..13fd4a972c --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt @@ -0,0 +1,54 @@ +/* + * 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.internal.auth.db.migration + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyOrder +import io.realm.DynamicRealm +import io.realm.RealmObjectSchema +import io.realm.RealmSchema +import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields + +class Fake005MigrationRealm { + + val instance: DynamicRealm = mockk() + + private val schema: RealmSchema = mockk() + private val objectSchema: RealmObjectSchema = mockk() + + init { + every { instance.schema } returns schema + every { schema.get("SessionParamsEntity") } returns objectSchema + every { objectSchema.addField(any(), any()) } returns objectSchema + every { objectSchema.setRequired(any(), any()) } returns objectSchema + every { objectSchema.transform(any()) } returns objectSchema + } + + fun verifyLoginTypeAdded() { + verifyLoginTypeFieldAddedAndTransformed() + } + + private fun verifyLoginTypeFieldAddedAndTransformed() { + verifyOrder { + objectSchema["SessionParamsEntity"] + objectSchema.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java) + objectSchema.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true) + objectSchema.transform(any()) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt new file mode 100644 index 0000000000..f1cb4071fd --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt @@ -0,0 +1,48 @@ +/* + * 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.internal.auth.db.sessionparams + +import com.squareup.moshi.JsonAdapter +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity +import org.matrix.android.sdk.test.fixtures.CredentialsFixture.aCredentials + +internal class FakeCredentialsJsonAdapter { + + val instance: JsonAdapter = mockk() + + init { + every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns credentials + every { instance.toJson(sessionParams.credentials) } returns CREDENTIALS_JSON + } + + fun givenNullDeserialization() { + every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null + } + + fun givenNullSerialization() { + every { instance.toJson(credentials) } returns null + } + + companion object { + val credentials = aCredentials() + const val CREDENTIALS_JSON = "credentials_json" + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt new file mode 100644 index 0000000000..f85d6e2778 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt @@ -0,0 +1,47 @@ +/* + * 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.internal.auth.db.sessionparams + +import com.squareup.moshi.JsonAdapter +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity + +internal class FakeHomeServerConnectionConfigJsonAdapter { + + val instance: JsonAdapter = mockk() + + init { + every { instance.fromJson(sessionParamsEntity.homeServerConnectionConfigJson) } returns homeServerConnectionConfig + every { instance.toJson(sessionParams.homeServerConnectionConfig) } returns HOME_SERVER_CONNECTION_CONFIG_JSON + } + + fun givenNullDeserialization() { + every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null + } + + fun givenNullSerialization() { + every { instance.toJson(homeServerConnectionConfig) } returns null + } + + companion object { + val homeServerConnectionConfig = HomeServerConnectionConfig.Builder().withHomeServerUri("homeserver").build() + const val HOME_SERVER_CONNECTION_CONFIG_JSON = "home_server_connection_config_json" + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt new file mode 100644 index 0000000000..ed0ddb1179 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt @@ -0,0 +1,86 @@ +/* + * 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.internal.auth.db.sessionparams + +import android.net.Uri +import com.squareup.moshi.Moshi +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.internal.auth.db.SessionParamsEntity +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.CREDENTIALS_JSON +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.credentials +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.HOME_SERVER_CONNECTION_CONFIG_JSON +import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.homeServerConnectionConfig +import org.matrix.android.sdk.test.fixtures.SessionParamsEntityFixture.aSessionParamsEntity +import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams + +internal class FakeSessionParamsMapperMoshi { + + val instance: Moshi = mockk() + private val credentialsJsonAdapter = FakeCredentialsJsonAdapter() + private val homeServerConnectionConfigAdapter = FakeHomeServerConnectionConfigJsonAdapter() + + init { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockk() + every { instance.adapter(Credentials::class.java) } returns credentialsJsonAdapter.instance + every { instance.adapter(HomeServerConnectionConfig::class.java) } returns homeServerConnectionConfigAdapter.instance + } + + fun assertSessionParamsWasMappedSuccessfully(sessionParams: SessionParams?) { + sessionParams shouldBeEqualTo SessionParams( + credentials, + homeServerConnectionConfig, + sessionParamsEntity.isTokenValid, + LoginType.fromName(sessionParamsEntity.loginType) + ) + } + + fun assertSessionParamsIsNull(sessionParams: SessionParams?) { + sessionParams.shouldBeNull() + } + + fun assertSessionParamsEntityWasMappedSuccessfully(sessionParamsEntity: SessionParamsEntity?) { + sessionParamsEntity shouldBeEqualTo SessionParamsEntity( + sessionParams.credentials.sessionId(), + sessionParams.userId, + CREDENTIALS_JSON, + HOME_SERVER_CONNECTION_CONFIG_JSON, + sessionParams.isTokenValid, + sessionParams.loginType.name, + ) + } + + fun assertSessionParamsEntityIsNull(sessionParamsEntity: SessionParamsEntity?) { + sessionParamsEntity.shouldBeNull() + } + + companion object { + val sessionParams = aSessionParams() + val sessionParamsEntity = aSessionParamsEntity() + val nullSessionParams: SessionParams? = null + val nullSessionParamsEntity: SessionParamsEntity? = null + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt new file mode 100644 index 0000000000..2e7b36ff63 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt @@ -0,0 +1,38 @@ +/* + * 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.fixtures + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.DiscoveryInformation + +object CredentialsFixture { + fun aCredentials( + userId: String = "", + accessToken: String = "", + refreshToken: String? = null, + homeServer: String? = null, + deviceId: String? = null, + discoveryInformation: DiscoveryInformation? = null, + ) = Credentials( + userId, + accessToken, + refreshToken, + homeServer, + deviceId, + discoveryInformation, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.kt new file mode 100644 index 0000000000..c929a27d23 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.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.test.fixtures + +import org.matrix.android.sdk.api.auth.data.DiscoveryInformation +import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig + +object DiscoveryInformationFixture { + fun aDiscoveryInformation( + homeServer: WellKnownBaseConfig? = null, + identityServer: WellKnownBaseConfig? = null, + ) = DiscoveryInformation( + homeServer, + identityServer + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt new file mode 100644 index 0000000000..bbea232a22 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt @@ -0,0 +1,37 @@ +/* + * 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.fixtures + +import org.matrix.android.sdk.internal.auth.db.SessionParamsEntity + +internal object SessionParamsEntityFixture { + fun aSessionParamsEntity( + sessionId: String = "", + userId: String = "", + credentialsJson: String = "", + homeServerConnectionConfigJson: String = "", + isTokenValid: Boolean = true, + loginType: String = "", + ) = SessionParamsEntity( + sessionId, + userId, + credentialsJson, + homeServerConnectionConfigJson, + isTokenValid, + loginType, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt new file mode 100644 index 0000000000..5cbbe1a47a --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt @@ -0,0 +1,37 @@ +/* + * 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.fixtures + +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.test.fixtures.CredentialsFixture.aCredentials + +object SessionParamsFixture { + fun aSessionParams( + credentials: Credentials = aCredentials(), + homeServerConnectionConfig: HomeServerConnectionConfig = HomeServerConnectionConfig.Builder().withHomeServerUri("homeserver").build(), + isTokenValid: Boolean = false, + loginType: LoginType = LoginType.UNKNOWN, + ) = SessionParams( + credentials, + homeServerConnectionConfig, + isTokenValid, + loginType, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt new file mode 100644 index 0000000000..a33308dbd6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt @@ -0,0 +1,27 @@ +/* + * 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.fixtures + +import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig + +object WellKnownBaseConfigFixture { + fun aWellKnownBaseConfig( + baseUrl: String? = null, + ) = WellKnownBaseConfig( + baseUrl, + ) +} diff --git a/settings.gradle b/settings.gradle index 0f537ed48a..782d2caf4a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,3 @@ include ':library:attachment-viewer' include ':library:diff-match-patch' include ':library:multipicker' include ':matrix-sdk-android-flow' -include ':library:opusencoder' diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh index 910616176c..79a42083d3 100755 --- a/tools/check/check_code_quality.sh +++ b/tools/check/check_code_quality.sh @@ -16,6 +16,21 @@ # limitations under the License. # +####################################################################################################################### +# Check frozen class modification +####################################################################################################################### + +echo "Check if frozen class modified" +git diff "HEAD@{1}" --name-only | grep -e OlmInboundGroupSessionWrapper.kt -e OlmInboundGroupSessionWrapper2.kt +FROZEN_CHANGED=$? +if [ ${FROZEN_CHANGED} -eq 0 ]; then + echo "❌ FROZEN CLASS CHANGED ERROR" + exit 1 +else + echo "Frozen check OK" +fi + + ####################################################################################################################### # Check drawable quantity ####################################################################################################################### diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 962a14843d..b12f15fa5d 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -180,3 +180,8 @@ System\.currentTimeMillis\(\)===2 ### Remove extra space between the name and the description \* @\w+ \w+ + + +### Please use the MenuProvider interface now +onCreateOptionsMenu +onOptionsItemSelected +onPrepareOptionsMenu diff --git a/vector/build.gradle b/vector/build.gradle index 8e073c7f38..cf32d45a1c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -35,7 +35,7 @@ ext.versionMinor = 4 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 27 +ext.versionPatch = 28 ext.scVersion = 55 @@ -353,8 +353,6 @@ android { } dependencies { - implementation project(':library:opusencoder') - implementation project(":vector-config") implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-flow") @@ -394,6 +392,8 @@ dependencies { implementation libs.androidx.datastorepreferences + // Opus Encoder + implementation libs.element.opusencoder // Log implementation libs.jakewharton.timber diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt new file mode 100644 index 0000000000..9e23e76f0c --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt @@ -0,0 +1,91 @@ +/* + * 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.graphics.Canvas +import android.graphics.Paint +import android.text.Layout +import android.text.Spannable +import androidx.core.text.getSpans +import im.vector.app.features.html.HtmlCodeSpan +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.noties.markwon.core.spans.EmphasisSpan +import io.noties.markwon.core.spans.OrderedListItemSpan +import io.noties.markwon.core.spans.StrongEmphasisSpan + +fun Spannable.toTestSpan(): String { + var output = toString() + readSpansWithContent().forEach { + val tags = it.span.readTags() + val remappedContent = it.span.remapContent(source = this, originalContent = it.content) + output = output.replace(it.content, "${tags.open}$remappedContent${tags.close}") + } + return output +} + +private fun Spannable.readSpansWithContent() = getSpans().map { span -> + val start = getSpanStart(span) + val end = getSpanEnd(span) + SpanWithContent( + content = substring(start, end), + span = span + ) +}.reversed() + +private fun Any.readTags(): SpanTags { + return when (this::class) { + OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]") + HtmlCodeSpan::class -> SpanTags("[code]", "[/code]") + StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]") + EmphasisSpan::class -> SpanTags("[italic]", "[/italic]") + else -> throw IllegalArgumentException("Unknown ${this::class}") + } +} + +private fun Any.remapContent(source: CharSequence, originalContent: String): String { + return when (this::class) { + OrderedListItemSpan::class -> { + val prefix = (this as OrderedListItemSpan).collectNumber(source) + "$prefix$originalContent" + } + else -> originalContent + } +} + +private fun OrderedListItemSpan.collectNumber(text: CharSequence): String { + val fakeCanvas = mockk() + val fakeLayout = mockk() + justRun { fakeCanvas.drawText(any(), any(), any(), any()) } + val paint = Paint() + drawLeadingMargin(fakeCanvas, paint, 0, 0, 0, 0, 0, text, 0, text.length - 1, true, fakeLayout) + val slot = slot() + verify { fakeCanvas.drawText(capture(slot), any(), any(), any()) } + return slot.captured +} + +private data class SpanTags( + val open: String, + val close: String, +) + +private data class SpanWithContent( + val content: String, + val span: Any +) 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 new file mode 100644 index 0000000000..41c0f51322 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt @@ -0,0 +1,82 @@ +/* + * 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.html + +import androidx.core.text.toSpannable +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.toTestSpan +import im.vector.app.features.settings.VectorPreferences +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.text.Typography.nbsp + +@RunWith(JUnit4::class) +class EventHtmlRendererTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val fakeVectorPreferences = mockk().also { + every { it.latexMathsIsEnabled() } returns false + } + + private val renderer = EventHtmlRenderer( + MatrixHtmlPluginConfigure(ColorProvider(context), context.resources), + context, + fakeVectorPreferences + ) + + @Test + fun takesInitialListPositionIntoAccount() { + val result = """
  1. first entry
""".renderAsTestSpan() + + result shouldBeEqualTo "[list item]5.${nbsp}first entry[/list item]\n" + } + + @Test + fun doesNotProcessMarkdownWithinCodeBlocks() { + val result = """__italic__ **bold**""".renderAsTestSpan() + + result shouldBeEqualTo "[code]__italic__ **bold**[/code]" + } + + @Test + fun doesNotProcessMarkdownBoldAndItalic() { + val result = """__italic__ **bold**""".renderAsTestSpan() + + result shouldBeEqualTo "__italic__ **bold**" + } + + @Test + fun processesHtmlWithinCodeBlocks() { + val result = """italic bold""".renderAsTestSpan() + + result shouldBeEqualTo "[code][italic]italic[/italic] [bold]bold[/bold][/code]" + } + + @Test + fun processesHtmlEntities() { + val result = """& < > ' """".renderAsTestSpan() + + result shouldBeEqualTo """& < > ' """" + } + + private fun String.renderAsTestSpan() = renderer.render(this).toSpannable().toTestSpan() +} 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 c7105b613f..61f745178c 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 @@ -19,6 +19,7 @@ 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.features.DefaultVectorFeatures import org.amshove.kluent.shouldBeInstanceOf import org.junit.After import org.junit.Test @@ -26,7 +27,7 @@ import org.junit.Test class VoiceRecorderProviderTests { private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val provider = VoiceRecorderProvider(context) + private val provider = VoiceRecorderProvider(context, DefaultVectorFeatures()) @After fun tearDown() { diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt b/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt index 8665081102..1e9b88c048 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/BooleanFeatureItem.kt @@ -23,11 +23,12 @@ 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 -@EpoxyModelClass(layout = im.vector.app.R.layout.item_feature) -abstract class BooleanFeatureItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BooleanFeatureItem : VectorEpoxyModel(R.layout.item_feature) { @EpoxyAttribute lateinit var feature: Feature.BooleanFeature diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 248d9d232b..9533e93ed1 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -70,6 +70,16 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.allowExternalUnifiedPushDistributors, factory = VectorFeatures::allowExternalUnifiedPushDistributors ), + createBooleanFeature( + label = "Force usage of OpusEncoder library", + key = DebugFeatureKeys.forceUsageOfOpusEncoder, + factory = VectorFeatures::forceUsageOfOpusEncoder + ), + createBooleanFeature( + label = "Start DM on first message", + key = DebugFeatureKeys.startDmOnFirstMsg, + factory = VectorFeatures::shouldStartDmOnFirstMessage + ), ) ) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 919cc6635e..1b178b5f48 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -66,6 +66,12 @@ class DebugVectorFeatures( override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing) ?: vectorFeatures.isScreenSharingEnabled() + override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder) + ?: vectorFeatures.forceUsageOfOpusEncoder() + + override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg) + ?: vectorFeatures.shouldStartDmOnFirstMessage() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -116,11 +122,13 @@ private fun > enumPreferencesKey(type: KClass) = stringPreference object DebugFeatureKeys { val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account") val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel") - val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel") + val onboardingUseCase = booleanPreferencesKey("onboarding-use-case") val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize") val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register") val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login") val allowExternalUnifiedPushDistributors = booleanPreferencesKey("allow-external-unified-push-distributors") val liveLocationSharing = booleanPreferencesKey("live-location-sharing") val screenSharing = booleanPreferencesKey("screen-sharing") + val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder") + val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg") } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt b/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt index a06147c4f8..5231e591da 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt @@ -23,11 +23,12 @@ 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 -@EpoxyModelClass(layout = im.vector.app.R.layout.item_feature) -abstract class EnumFeatureItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class EnumFeatureItem : VectorEpoxyModel(R.layout.item_feature) { @EpoxyAttribute lateinit var feature: Feature.EnumFeature<*> diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt b/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt index 9e0c013960..179ee35693 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt @@ -21,14 +21,15 @@ 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 me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation -@EpoxyModelClass(layout = im.vector.app.R.layout.item_sas_emoji) -abstract class SasEmojiItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SasEmojiItem : VectorEpoxyModel(R.layout.item_sas_emoji) { @EpoxyAttribute var index: Int = 0 @@ -51,9 +52,9 @@ abstract class SasEmojiItem : VectorEpoxyModel() { } class Holder : VectorEpoxyHolder() { - val indexView by bind(im.vector.app.R.id.sas_emoji_index) - val emojiView by bind(im.vector.app.R.id.sas_emoji) - val textView by bind(im.vector.app.R.id.sas_emoji_text) - val idView by bind(im.vector.app.R.id.sas_emoji_text_id) + val indexView by bind(R.id.sas_emoji_index) + val emojiView by bind(R.id.sas_emoji) + val textView by bind(R.id.sas_emoji_text) + val idView by bind(R.id.sas_emoji_text_id) } } diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index f9adc521c9..29dac6533e 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -43,7 +43,7 @@ diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt index d421c8bb87..61fd70adc1 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/service/FDroidGuardServiceStarter.kt @@ -33,7 +33,7 @@ class FDroidGuardServiceStarter @Inject constructor( if (preferences.isBackgroundSyncEnabled()) { try { Timber.i("## Sync: starting GuardService") - val intent = Intent(appContext, GuardService::class.java) + val intent = Intent(appContext, GuardAndroidService::class.java) ContextCompat.startForegroundService(appContext, intent) } catch (ex: Throwable) { Timber.e("## Sync: ERROR starting GuardService") @@ -42,7 +42,7 @@ class FDroidGuardServiceStarter @Inject constructor( } override fun stop() { - val intent = Intent(appContext, GuardService::class.java) + val intent = Intent(appContext, GuardAndroidService::class.java) appContext.stopService(intent) } } diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/service/GuardService.kt b/vector/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt similarity index 93% rename from vector/src/fdroid/java/im/vector/app/fdroid/service/GuardService.kt rename to vector/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt index 053cf87c17..f46b8f9820 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/service/GuardService.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/service/GuardAndroidService.kt @@ -18,7 +18,7 @@ package im.vector.app.fdroid.service import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.services.VectorService +import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.notifications.NotificationUtils import javax.inject.Inject @@ -29,7 +29,7 @@ import javax.inject.Inject * when the app is not in the foreground. */ @AndroidEntryPoint -class GuardService : VectorService() { +class GuardAndroidService : VectorAndroidService() { @Inject lateinit var notificationUtils: NotificationUtils diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 65115cdf40..9186771bc0 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -77,6 +77,7 @@ @@ -353,7 +355,7 @@ @@ -368,7 +370,7 @@ tools:ignore="Instantiatable" /> @@ -377,12 +379,12 @@ @@ -417,9 +419,9 @@ + android:exported="true"> diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 1f0cec390a..c35e35b1c0 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -111,6 +111,8 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFrag import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyWaitForEmailFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthPersonalizationCompleteFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthPhoneConfirmationFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthPhoneEntryFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment @@ -511,6 +513,16 @@ interface FragmentModule { @FragmentKey(FtueAuthEmailEntryFragment::class) fun bindFtueAuthEmailEntryFragment(fragment: FtueAuthEmailEntryFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthPhoneEntryFragment::class) + fun bindFtueAuthPhoneEntryFragment(fragment: FtueAuthPhoneEntryFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(FtueAuthPhoneConfirmationFragment::class) + fun bindFtueAuthPhoneConfirmationFragment(fragment: FtueAuthPhoneConfirmationFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthChooseDisplayNameFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index bc0bccfa1b..cbd34fa05b 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import android.content.res.Resources +import com.google.i18n.phonenumbers.PhoneNumberUtil import dagger.Binds import dagger.Module import dagger.Provides @@ -193,6 +194,9 @@ object VectorStaticModule { return analyticsConfig } + @Provides + fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance() + @Provides @Singleton fun providesBuildMeta() = BuildMeta() diff --git a/vector/src/main/java/im/vector/app/core/epoxy/BottomSheetDividerItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/BottomSheetDividerItem.kt index 2d65f0ce0c..7cdbaab0b7 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/BottomSheetDividerItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/BottomSheetDividerItem.kt @@ -18,7 +18,7 @@ package im.vector.app.core.epoxy import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_divider_on_surface) -abstract class BottomSheetDividerItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetDividerItem : VectorEpoxyModel(R.layout.item_divider_on_surface) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt index 2f32fafa9e..dc016c18ec 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt @@ -22,8 +22,8 @@ import com.airbnb.epoxy.EpoxyModelClass import com.google.android.material.checkbox.MaterialCheckBox import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_checkbox) -abstract class CheckBoxItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class CheckBoxItem : VectorEpoxyModel(R.layout.item_checkbox) { @EpoxyAttribute var checked: Boolean = false diff --git a/vector/src/main/java/im/vector/app/core/epoxy/DividerItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/DividerItem.kt index 73568444b6..54fd9f6e0b 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/DividerItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/DividerItem.kt @@ -18,7 +18,7 @@ package im.vector.app.core.epoxy import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_divider) -abstract class DividerItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class DividerItem : VectorEpoxyModel(R.layout.item_divider) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/ErrorWithRetryItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/ErrorWithRetryItem.kt index 8d2b2be6c2..4f0767c068 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/ErrorWithRetryItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/ErrorWithRetryItem.kt @@ -23,8 +23,8 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_error_retry) -abstract class ErrorWithRetryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ErrorWithRetryItem : VectorEpoxyModel(R.layout.item_error_retry) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/ExpandableTextItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/ExpandableTextItem.kt index d526e92b7c..359ea4bbd6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/ExpandableTextItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/ExpandableTextItem.kt @@ -28,8 +28,8 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.extensions.copyOnLongClick -@EpoxyModelClass(layout = R.layout.item_expandable_textview) -abstract class ExpandableTextItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ExpandableTextItem : VectorEpoxyModel(R.layout.item_expandable_textview) { @EpoxyAttribute lateinit var content: String diff --git a/vector/src/main/java/im/vector/app/core/epoxy/HelpFooterItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/HelpFooterItem.kt index bb39bd41d6..c3ab37d19d 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/HelpFooterItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/HelpFooterItem.kt @@ -21,8 +21,8 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_help_footer) -abstract class HelpFooterItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class HelpFooterItem : VectorEpoxyModel(R.layout.item_help_footer) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/LoadingItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/LoadingItem.kt index 5cf87bbf78..63aea7dbaf 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/LoadingItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/LoadingItem.kt @@ -24,8 +24,8 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_loading) -abstract class LoadingItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoadingItem : VectorEpoxyModel(R.layout.item_loading) { @EpoxyAttribute var loadingText: String? = null @EpoxyAttribute var showLoader: Boolean = true diff --git a/vector/src/main/java/im/vector/app/core/epoxy/NoResultItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/NoResultItem.kt index d38ed93225..bddcbd0846 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/NoResultItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/NoResultItem.kt @@ -21,8 +21,8 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_no_result) -abstract class NoResultItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class NoResultItem : VectorEpoxyModel(R.layout.item_no_result) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/SquareLoadingItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/SquareLoadingItem.kt index 7844a75dae..75ce203927 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/SquareLoadingItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/SquareLoadingItem.kt @@ -19,8 +19,8 @@ package im.vector.app.core.epoxy import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_loading_square) -abstract class SquareLoadingItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SquareLoadingItem : VectorEpoxyModel(R.layout.item_loading_square) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt index c51573bf21..a642981d92 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt @@ -22,8 +22,8 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents -@EpoxyModelClass(layout = R.layout.item_timeline_empty) -abstract class TimelineEmptyItem : VectorEpoxyModel(), ItemWithEvents { +@EpoxyModelClass +abstract class TimelineEmptyItem : VectorEpoxyModel(R.layout.item_timeline_empty), ItemWithEvents { @EpoxyAttribute lateinit var eventId: String @EpoxyAttribute var notBlank: Boolean = false diff --git a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt index 9bd16b09a3..66eca2569d 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt @@ -17,6 +17,7 @@ package im.vector.app.core.epoxy import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.VisibilityState import kotlinx.coroutines.CoroutineScope @@ -27,12 +28,16 @@ import kotlinx.coroutines.cancelChildren /** * EpoxyModelWithHolder which can listen to visibility state change. */ -abstract class VectorEpoxyModel : EpoxyModelWithHolder() { +abstract class VectorEpoxyModel( + @LayoutRes private val layoutId: Int +) : EpoxyModelWithHolder() { protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null + final override fun getDefaultLayout() = layoutId + @CallSuper override fun bind(holder: H) { super.bind(holder) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/ZeroItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/ZeroItem.kt index a65e37f96b..dbdb4ddbd9 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/ZeroItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/ZeroItem.kt @@ -23,8 +23,8 @@ import im.vector.app.R * Item of size (0, 0). * It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes. */ -@EpoxyModelClass(layout = R.layout.item_zero) -abstract class ZeroItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ZeroItem : VectorEpoxyModel(R.layout.item_zero) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt index ed3d55fca9..0656726e7d 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -39,8 +39,8 @@ import im.vector.app.features.themes.ThemeUtils /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action) -abstract class BottomSheetActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetActionItem : VectorEpoxyModel(R.layout.item_bottom_sheet_action) { @EpoxyAttribute @DrawableRes diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index b90956ad9e..bb1b0fbd7b 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -43,8 +43,8 @@ import org.matrix.android.sdk.api.util.MatrixItem /** * A message preview for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview) -abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.layout.item_bottom_sheet_message_preview) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt index 3f6b23a85f..1b7c9b3409 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetQuickReactionsItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.onClick /** * A quick reaction list for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction) -abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel(R.layout.item_bottom_sheet_quick_reaction) { @EpoxyAttribute lateinit var fontProvider: EmojiCompatFontProvider diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt index 10bf92b7e7..2fe276fe01 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt @@ -33,8 +33,8 @@ import im.vector.app.core.extensions.setTextOrHide /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_radio) -abstract class BottomSheetRadioActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetRadioActionItem : VectorEpoxyModel(R.layout.item_bottom_sheet_radio) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt index 44e8f32cfd..c4e48335b6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt @@ -39,8 +39,8 @@ import org.matrix.android.sdk.api.util.MatrixItem /** * A room preview for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview) -abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel(R.layout.item_bottom_sheet_room_preview) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt index aa134ac7c4..4b3073f8f2 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A send state for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status) -abstract class BottomSheetSendStateItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetSendStateItem : VectorEpoxyModel(R.layout.item_bottom_sheet_message_status) { @EpoxyAttribute var showProgress: Boolean = false 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 956e1de92c..9c5ad49339 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 @@ -17,6 +17,7 @@ package im.vector.app.core.epoxy.profiles import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.core.epoxy.ClickListener @@ -28,7 +29,7 @@ import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.util.MatrixItem -abstract class BaseProfileMatrixItem : VectorEpoxyModel() { +abstract class BaseProfileMatrixItem(@LayoutRes layoutId: Int) : VectorEpoxyModel(layoutId) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var editable: Boolean = true diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileActionItem.kt index 33e5172146..12e83ea80d 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileActionItem.kt @@ -33,8 +33,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_profile_action) -abstract class ProfileActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ProfileActionItem : VectorEpoxyModel(R.layout.item_profile_action) { @EpoxyAttribute lateinit var title: String diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItem.kt index 90e81ceb26..7e89db24fa 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.ui.views.PresenceStateImageView import im.vector.app.core.ui.views.ShieldImageView -@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) -abstract class ProfileMatrixItem : BaseProfileMatrixItem() { +@EpoxyModelClass +abstract class ProfileMatrixItem : BaseProfileMatrixItem(R.layout.item_profile_matrix_item) { open class Holder : VectorEpoxyHolder() { val titleView by bind(R.id.matrixItemTitle) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevel.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevel.kt index 453f402496..baf4605f04 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevel.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevel.kt @@ -20,10 +20,9 @@ package im.vector.app.core.epoxy.profiles 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.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) +@EpoxyModelClass abstract class ProfileMatrixItemWithPowerLevel : ProfileMatrixItem() { @EpoxyAttribute var ignoredUser: Boolean = false diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt index 923fa80b55..cdb9791c56 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt @@ -19,10 +19,9 @@ package im.vector.app.core.epoxy.profiles import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R import org.matrix.android.sdk.api.session.presence.model.UserPresence -@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) +@EpoxyModelClass abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() { @EpoxyAttribute var showPresence: Boolean = true diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithProgress.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithProgress.kt index 4966032dde..1b0274b32d 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithProgress.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithProgress.kt @@ -23,8 +23,8 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_profile_matrix_item_progress) -abstract class ProfileMatrixItemWithProgress : BaseProfileMatrixItem() { +@EpoxyModelClass +abstract class ProfileMatrixItemWithProgress : BaseProfileMatrixItem(R.layout.item_profile_matrix_item_progress) { @EpoxyAttribute var inProgress: Boolean = true diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileSectionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileSectionItem.kt index b38342c057..f607537d54 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileSectionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileSectionItem.kt @@ -23,8 +23,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_profile_section) -abstract class ProfileSectionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ProfileSectionItem : VectorEpoxyModel(R.layout.item_profile_section) { @EpoxyAttribute lateinit var title: String diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/BottomSheetRadioButtonItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/BottomSheetRadioButtonItem.kt index fbfc97ab26..8b305e3ed6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/BottomSheetRadioButtonItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/BottomSheetRadioButtonItem.kt @@ -34,8 +34,8 @@ import im.vector.app.features.themes.ThemeUtils /** * SC: copy of RadioButtonItem, but with extra icon + different layout */ -@EpoxyModelClass(layout = R.layout.bottom_sheet_item_radio) -abstract class BottomSheetRadioButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetRadioButtonItem : VectorEpoxyModel(R.layout.bottom_sheet_item_radio) { @EpoxyAttribute var title: CharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt index 4608f2b1ce..203a10a6fe 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt @@ -25,8 +25,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextWithColoredPart -@EpoxyModelClass(layout = R.layout.item_notifications_footer) -abstract class NotificationSettingsFooterItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class NotificationSettingsFooterItem : VectorEpoxyModel(R.layout.item_notifications_footer) { @EpoxyAttribute var encrypted: Boolean = false diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt index 128d34a6b0..b07ce5af4c 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setAttributeTintedImageResource -@EpoxyModelClass(layout = R.layout.item_radio) -abstract class RadioButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RadioButtonItem : VectorEpoxyModel(R.layout.item_radio) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt index 2dfe7be2e6..3302d02425 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt @@ -24,8 +24,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_text_header) -abstract class TextHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class TextHeaderItem : VectorEpoxyModel(R.layout.item_text_header) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt index 41016365c0..d3ee765780 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -16,12 +16,18 @@ package im.vector.app.core.extensions +import android.os.Build import android.text.Editable import android.view.View import android.view.inputmethod.EditorInfo +import androidx.autofill.HintConstants +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.vector.app.core.platform.SimpleTextWatcher +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.textChanges fun TextInputLayout.editText() = this.editText!! @@ -37,11 +43,18 @@ fun TextInputLayout.content() = editText().text.toString() fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() -fun TextInputLayout.associateContentStateWith(button: View) { +fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) { + editText().textChanges() + .onEach { error = null } + .launchIn(lifecycleOwner.lifecycleScope) +} + +fun TextInputLayout.associateContentStateWith(button: View, enabledPredicate: (String) -> Boolean = { it.isNotEmpty() }) { + button.isEnabled = enabledPredicate(content()) editText().addTextChangedListener(object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { val newContent = s.toString() - button.isEnabled = newContent.isNotEmpty() + button.isEnabled = enabledPredicate(newContent) } }) } @@ -68,3 +81,12 @@ fun TextInputLayout.setOnFocusLostListener(action: () -> Unit) { } } } + +fun TextInputLayout.autofillPhoneNumber() = setAutofillHint(HintConstants.AUTOFILL_HINT_PHONE_NUMBER) +fun TextInputLayout.autofillEmail() = setAutofillHint(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS) + +private fun TextInputLayout.setAutofillHint(hintType: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setAutofillHints(hintType) + } +} 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 571d2d38c0..9bad0f8e90 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 @@ -19,11 +19,11 @@ package im.vector.app.core.platform import android.annotation.SuppressLint import android.app.Activity import android.content.Context -import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.WindowInsetsController @@ -31,15 +31,18 @@ import android.view.WindowManager import android.widget.TextView import androidx.annotation.CallSuper import androidx.annotation.MainThread -import androidx.annotation.MenuRes import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.app.MultiWindowModeChangedInfo import androidx.core.content.ContextCompat +import androidx.core.util.Consumer +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding @@ -86,6 +89,7 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.receivers.DebugReceiver import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.InitialSyncRequestReason @@ -199,6 +203,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver supportFragmentManager.fragmentFactory = fragmentFactory viewModelFactory = activityEntryPoint.viewModelFactory() super.onCreate(savedInstanceState) + addOnMultiWindowModeChangedListener(onMultiWindowModeChangedListener) + setupMenu() configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) bugReporter = singletonEntryPoint.bugReporter() pinLocker = singletonEntryPoint.pinLocker() @@ -249,6 +255,32 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver } } + private fun setupMenu() { + // Always add a MenuProvider to handle the back action from the Toolbar + val vectorMenuProvider = this as? VectorMenuProvider + addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + vectorMenuProvider?.let { + menuInflater.inflate(it.getMenuRes(), menu) + it.handlePostCreateMenu(menu) + } + } + + override fun onPrepareMenu(menu: Menu) { + vectorMenuProvider?.handlePrepareMenu(menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return vectorMenuProvider?.handleMenuItemSelected(menuItem).orFalse() || + handleMenuItemHome(menuItem) + } + }, + this, + Lifecycle.State.RESUMED + ) + } + /** * This method has to be called for the font size setting be supported correctly. */ @@ -332,6 +364,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver } override fun onDestroy() { + removeOnMultiWindowModeChangedListener(onMultiWindowModeChangedListener) super.onDestroy() Timber.i("onDestroy Activity ${javaClass.simpleName}") } @@ -417,11 +450,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver } } - override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) { - super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) - - Timber.w("onMultiWindowModeChanged. isInMultiWindowMode: $isInMultiWindowMode") - bugReporter.inMultiWindowMode = isInMultiWindowMode + private val onMultiWindowModeChangedListener = Consumer { + Timber.w("onMultiWindowModeChanged. isInMultiWindowMode: ${it.isInMultiWindowMode}") + bugReporter.inMultiWindowMode = it.isInMultiWindowMode } protected fun createFragment(fragmentClass: Class, argsParcelable: Parcelable? = null): Fragment { @@ -463,28 +494,14 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver } } - /* ========================================================================================== - * MENU MANAGEMENT - * ========================================================================================== */ - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val menuRes = getMenuRes() - - if (menuRes != -1) { - menuInflater.inflate(menuRes, menu) - return true + private fun handleMenuItemHome(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed(true) + true + } + else -> false } - - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed(true) - return true - } - - return super.onOptionsItemSelected(item) } override fun onBackPressed() { @@ -587,9 +604,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver @StringRes open fun getTitleRes() = -1 - @MenuRes - open fun getMenuRes() = -1 - /** * Return a object containing other themes for this activity. */ diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 38667b774f..340c906a6d 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -22,12 +22,16 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding @@ -126,9 +130,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (getMenuRes() != -1) { - setHasOptionsMenu(true) - } + Timber.i("onCreate Fragment ${javaClass.simpleName}") } final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -158,6 +160,31 @@ abstract class VectorBaseFragment : Fragment(), MavericksView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Timber.i("onViewCreated Fragment ${javaClass.simpleName}") + setupMenu() + } + + private fun setupMenu() { + if (this !is VectorMenuProvider) return + if (getMenuRes() == -1) return + val menuHost: MenuHost = requireActivity() + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(getMenuRes(), menu) + handlePostCreateMenu(menu) + } + + override fun onPrepareMenu(menu: Menu) { + handlePrepareMenu(menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return handleMenuItemSelected(menuItem) + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) } open fun showLoading(message: CharSequence?) { @@ -270,16 +297,6 @@ abstract class VectorBaseFragment : Fragment(), MavericksView * MENU MANAGEMENT * ========================================================================================== */ - open fun getMenuRes() = -1 - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - val menuRes = getMenuRes() - - if (menuRes != -1) { - inflater.inflate(menuRes, menu) - } - } - // This should be provided by the framework protected fun invalidateOptionsMenu() = requireActivity().invalidateOptionsMenu() diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorMenuProvider.kt b/vector/src/main/java/im/vector/app/core/platform/VectorMenuProvider.kt new file mode 100644 index 0000000000..05d710a185 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/VectorMenuProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.platform + +import android.view.Menu +import android.view.MenuItem +import androidx.annotation.MenuRes + +/** + * Let your Activity of Fragment implement this interface if they provide a Menu. + */ +interface VectorMenuProvider { + @MenuRes + fun getMenuRes(): Int + + // No op by default + fun handlePostCreateMenu(menu: Menu) {} + + // No op by default + fun handlePrepareMenu(menu: Menu) {} + + fun handleMenuItemSelected(item: MenuItem): Boolean +} diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt similarity index 97% rename from vector/src/main/java/im/vector/app/core/services/CallService.kt rename to vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt index 4edc4d6ace..7a078ce1c8 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt @@ -51,7 +51,7 @@ private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP) * Foreground service to manage calls. */ @AndroidEntryPoint -class CallService : VectorService() { +class CallAndroidService : VectorAndroidService() { private val connections = mutableMapOf() private val knownCalls = mutableMapOf() @@ -98,7 +98,7 @@ class CallService : VectorService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.tag(loggerTag.value).v("onStartCommand $intent") if (mediaSession == null) { - mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply { + mediaSession = MediaSessionCompat(applicationContext, CallAndroidService::class.java.name).apply { setCallback(mediaSessionButtonCallback) } } @@ -326,7 +326,7 @@ class CallService : VectorService() { callId: String, isInBackground: Boolean ) { - val intent = Intent(context, CallService::class.java) + val intent = Intent(context, CallAndroidService::class.java) .apply { action = ACTION_INCOMING_RINGING_CALL putExtra(EXTRA_CALL_ID, callId) @@ -339,7 +339,7 @@ class CallService : VectorService() { context: Context, callId: String ) { - val intent = Intent(context, CallService::class.java) + val intent = Intent(context, CallAndroidService::class.java) .apply { action = ACTION_OUTGOING_RINGING_CALL putExtra(EXTRA_CALL_ID, callId) @@ -351,7 +351,7 @@ class CallService : VectorService() { context: Context, callId: String ) { - val intent = Intent(context, CallService::class.java) + val intent = Intent(context, CallAndroidService::class.java) .apply { action = ACTION_ONGOING_CALL putExtra(EXTRA_CALL_ID, callId) @@ -365,7 +365,7 @@ class CallService : VectorService() { endCallReason: EndCallReason, rejected: Boolean ) { - val intent = Intent(context, CallService::class.java) + val intent = Intent(context, CallAndroidService::class.java) .apply { action = ACTION_CALL_TERMINATED putExtra(EXTRA_CALL_ID, callId) @@ -377,8 +377,8 @@ class CallService : VectorService() { } inner class CallServiceBinder : Binder() { - fun getCallService(): CallService { - return this@CallService + fun getCallService(): CallAndroidService { + return this@CallAndroidService } } } diff --git a/vector/src/main/java/im/vector/app/core/services/VectorService.kt b/vector/src/main/java/im/vector/app/core/services/VectorAndroidService.kt similarity index 93% rename from vector/src/main/java/im/vector/app/core/services/VectorService.kt rename to vector/src/main/java/im/vector/app/core/services/VectorAndroidService.kt index cc816c21a1..f30a74e9de 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorAndroidService.kt @@ -22,9 +22,9 @@ import android.os.IBinder import timber.log.Timber /** - * Parent class for all services. + * Parent class for all Android Services. */ -abstract class VectorService : Service() { +abstract class VectorAndroidService : Service() { /** * Tells if the service self destroyed. diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt index 27fb634480..bd1a2baac2 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt @@ -27,8 +27,8 @@ import im.vector.app.core.extensions.setTextOrHide /** * A title for bottom sheet, with an optional subtitle. It does not include the bottom separator. */ -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_title) -abstract class BottomSheetTitleItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetTitleItem : VectorEpoxyModel(R.layout.item_bottom_sheet_title) { @EpoxyAttribute lateinit var title: String diff --git a/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt index 95c1a4457d..254458fcdc 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt @@ -28,8 +28,10 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic button list item. */ -@EpoxyModelClass(layout = R.layout.item_positive_destrutive_buttons) -abstract class ButtonPositiveDestructiveButtonBarItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ButtonPositiveDestructiveButtonBarItem : VectorEpoxyModel( + R.layout.item_positive_destrutive_buttons +) { @EpoxyAttribute var positiveText: EpoxyCharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt index d4838289a6..a3b6b40616 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt @@ -33,8 +33,8 @@ import im.vector.app.features.themes.ThemeUtils /** * A generic button list item. */ -@EpoxyModelClass(layout = R.layout.item_generic_button) -abstract class GenericButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericButtonItem : VectorEpoxyModel(R.layout.item_generic_button) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt index 4cc39af0a5..8ae45d3af1 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericEmptyWithActionItem.kt @@ -35,8 +35,8 @@ import im.vector.app.core.extensions.setTextOrHide /** * A generic list item to display when there is no results, with an optional CTA. */ -@EpoxyModelClass(layout = R.layout.item_generic_empty_state) -abstract class GenericEmptyWithActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericEmptyWithActionItem : VectorEpoxyModel(R.layout.item_generic_empty_state) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt index 8dbdcc473e..a73647f4e7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt @@ -36,8 +36,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence * Can display an accessory on the right, that can be an image or an indeterminate progress. * If provided with an action, will display a button at the bottom of the list item. */ -@EpoxyModelClass(layout = R.layout.item_generic_footer) -abstract class GenericFooterItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericFooterItem : VectorEpoxyModel(R.layout.item_generic_footer) { @EpoxyAttribute var text: EpoxyCharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericHeaderItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericHeaderItem.kt index b4b0211b91..2123b4c639 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericHeaderItem.kt @@ -28,8 +28,8 @@ import im.vector.app.features.themes.ThemeUtils /** * A generic list item header left aligned with notice color. */ -@EpoxyModelClass(layout = R.layout.item_generic_header) -abstract class GenericHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericHeaderItem : VectorEpoxyModel(R.layout.item_generic_header) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt index 7b00001e4c..e9c4ebe0ee 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt @@ -38,8 +38,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence * Can display an accessory on the right, that can be an image or an indeterminate progress. * If provided with an action, will display a button at the bottom of the list item. */ -@EpoxyModelClass(layout = R.layout.item_generic_list) -abstract class GenericItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericItem : VectorEpoxyModel(R.layout.item_generic_list) { @EpoxyAttribute var title: EpoxyCharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericLoaderItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericLoaderItem.kt index b458f10680..2077346442 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericLoaderItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericLoaderItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A generic list item header left aligned with notice color. */ -@EpoxyModelClass(layout = R.layout.item_generic_loader) -abstract class GenericLoaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericLoaderItem : VectorEpoxyModel(R.layout.item_generic_loader) { // Maybe/Later add some style configuration, SMALL/BIG ? diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt index a51c1b70ed..2a321ac4b0 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt @@ -36,8 +36,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item with a rounded corner background and an optional icon. */ -@EpoxyModelClass(layout = R.layout.item_generic_pill_footer) -abstract class GenericPillItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericPillItem : VectorEpoxyModel(R.layout.item_generic_pill_footer) { @EpoxyAttribute var text: EpoxyCharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt index 753b085d99..948f9036a7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.onClick /** * A generic button list item. */ -@EpoxyModelClass(layout = R.layout.item_positive_button) -abstract class GenericPositiveButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericPositiveButtonItem : VectorEpoxyModel(R.layout.item_positive_button) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt index e392c0bdf2..8d2e55d198 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericProgressBarItem.kt @@ -25,8 +25,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A generic progress bar item. */ -@EpoxyModelClass(layout = R.layout.item_generic_progress) -abstract class GenericProgressBarItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericProgressBarItem : VectorEpoxyModel(R.layout.item_generic_progress) { @EpoxyAttribute var progress: Int = 0 diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt index e633b633a7..8b82bf4684 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt @@ -38,8 +38,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence * Can display an accessory on the right, that can be an image or an indeterminate progress. * If provided with an action, will display a button at the bottom of the list item. */ -@EpoxyModelClass(layout = R.layout.item_generic_with_value) -abstract class GenericWithValueItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GenericWithValueItem : VectorEpoxyModel(R.layout.item_generic_with_value) { @EpoxyAttribute var title: EpoxyCharSequence? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt index ec99c7c215..ffb152ecff 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A generic item with empty space. */ -@EpoxyModelClass(layout = R.layout.item_vertical_margin) -abstract class VerticalMarginItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class VerticalMarginItem : VectorEpoxyModel(R.layout.item_vertical_margin) { @EpoxyAttribute var heightInPx: Int = 0 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 68fe488391..3a8ffac543 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 @@ -76,6 +76,8 @@ fun openUrlInExternalBrowser(context: Context, url: String?) { fun openUrlInExternalBrowser(context: Context, uri: Uri?) { uri?.let { val browserIntent = Intent(Intent.ACTION_VIEW, it).apply { + // Open activity on browser task and not on element task + flags = Intent.FLAG_ACTIVITY_NEW_TASK putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName) putExtra(Browser.EXTRA_CREATE_NEW_TAB, true) } 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 6fe4beff95..eaacb0498e 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -30,6 +30,8 @@ interface VectorFeatures { fun isOnboardingCombinedLoginEnabled(): Boolean fun allowExternalUnifiedPushDistributors(): Boolean fun isScreenSharingEnabled(): Boolean + fun forceUsageOfOpusEncoder(): Boolean + fun shouldStartDmOnFirstMessage(): Boolean enum class OnboardingVariant { LEGACY, @@ -48,4 +50,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingCombinedLoginEnabled() = false override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS override fun isScreenSharingEnabled(): Boolean = true + override fun forceUsageOfOpusEncoder(): Boolean = false + override fun shouldStartDmOnFirstMessage(): Boolean = false } diff --git a/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt b/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt new file mode 100644 index 0000000000..0f2dad541c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt @@ -0,0 +1,37 @@ +/* + * 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.attachments + +import android.content.Context +import android.content.Intent +import im.vector.lib.multipicker.MultiPicker +import javax.inject.Inject + +class MultiPickerIncomingFiles @Inject constructor( + private val context: Context, +) { + + fun image(intent: Intent) = MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun video(intent: Intent) = MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun media(intent: Intent) = MultiPicker.get(MultiPicker.MEDIA).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun file(intent: Intent) = MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun audio(intent: Intent) = MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt b/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt index 06ca949025..18caba10d9 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt @@ -18,53 +18,36 @@ package im.vector.app.features.attachments import android.content.Context import android.content.Intent -import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAny +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeApplication +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeFile +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeText +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo import javax.inject.Inject -class ShareIntentHandler @Inject constructor() { +class ShareIntentHandler @Inject constructor( + private val multiPickerIncomingFiles: MultiPickerIncomingFiles, + private val context: Context, +) { /** * This methods aims to handle incoming share intents. * * @return true if it can handle the intent data, false otherwise */ - fun handleIncomingShareIntent(context: Context, intent: Intent, onFile: (List) -> Unit, onPlainText: (String) -> Unit): Boolean { + fun handleIncomingShareIntent(intent: Intent, onFile: (List) -> Unit, onPlainText: (String) -> Unit): Boolean { val type = intent.resolveType(context) ?: return false return when { - type == "text/plain" -> handlePlainText(intent, onPlainText) - type.startsWith("image") -> { - onFile( - MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - type.startsWith("video") -> { - onFile( - MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - type.startsWith("audio") -> { - onFile( - MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - - type.startsWith("application") || type.startsWith("file") || type.startsWith("text") || type.startsWith("*") -> { - onFile( - MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true + type == MimeTypes.PlainText -> handlePlainText(intent, onPlainText) + type.isMimeTypeImage() -> onFile(multiPickerIncomingFiles.image(intent)).let { true } + type.isMimeTypeVideo() -> onFile(multiPickerIncomingFiles.video(intent)).let { true } + type.isMimeTypeAudio() -> onFile(multiPickerIncomingFiles.audio(intent)).let { true } + type.isMimeTypeApplication() || type.isMimeTypeFile() || type.isMimeTypeText() || type.isMimeTypeAny() -> { + onFile(multiPickerIncomingFiles.file(intent)).let { true } } else -> false } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt index ae18d2561d..0298b2da17 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt @@ -18,6 +18,7 @@ package im.vector.app.features.attachments.preview import android.view.View import android.widget.ImageView +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -29,7 +30,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.platform.CheckableImageView import org.matrix.android.sdk.api.session.content.ContentAttachmentData -abstract class AttachmentPreviewItem : VectorEpoxyModel() { +abstract class AttachmentPreviewItem(@LayoutRes layoutId: Int) : VectorEpoxyModel(layoutId) { abstract val attachment: ContentAttachmentData @@ -52,8 +53,8 @@ abstract class AttachmentPreviewItem : VectorE } } -@EpoxyModelClass(layout = R.layout.item_attachment_miniature_preview) -abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem() { +@EpoxyModelClass +abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem(R.layout.item_attachment_miniature_preview) { @EpoxyAttribute override lateinit var attachment: ContentAttachmentData @@ -78,8 +79,8 @@ abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem() { +@EpoxyModelClass +abstract class AttachmentBigPreviewItem : AttachmentPreviewItem(R.layout.item_attachment_big_preview) { @EpoxyAttribute override lateinit var attachment: ContentAttachmentData 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 9176c60b91..e98facd1a0 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 @@ -44,6 +44,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.insertBeforeLast import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.time.Clock import im.vector.app.core.utils.OnSnapPositionChangeListener @@ -67,7 +68,9 @@ class AttachmentsPreviewFragment @Inject constructor( private val attachmentBigPreviewController: AttachmentBigPreviewController, private val colorProvider: ColorProvider, private val clock: Clock, -) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback { +) : VectorBaseFragment(), + AttachmentMiniaturePreviewController.Callback, + VectorMenuProvider { private val fragmentArgs: AttachmentsPreviewArgs by args() private val viewModel: AttachmentsPreviewViewModel by fragmentViewModel() @@ -97,7 +100,7 @@ class AttachmentsPreviewFragment @Inject constructor( } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.attachmentsPreviewRemoveAction -> { handleRemoveAction() @@ -107,20 +110,16 @@ class AttachmentsPreviewFragment @Inject constructor( handleEditAction() true } - else -> { - super.onOptionsItemSelected(item) - } + else -> false } } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { withState(viewModel) { state -> val editMenuItem = menu.findItem(R.id.attachmentsPreviewEditAction) val showEditMenuItem = state.attachments.getOrNull(state.currentAttachmentIndex)?.isEditable().orFalse() editMenuItem.setVisible(showEditMenuItem) } - - super.onPrepareOptionsMenu(menu) } override fun getMenuRes() = R.menu.vector_attachments_preview diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt index f287104415..0f40ef761d 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt @@ -23,8 +23,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_autocomplete_header_item) -abstract class AutocompleteHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class AutocompleteHeaderItem : VectorEpoxyModel(R.layout.item_autocomplete_header_item) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt index dba2661927..68668bcc6a 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt @@ -30,8 +30,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item) -abstract class AutocompleteMatrixItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class AutocompleteMatrixItem : VectorEpoxyModel(R.layout.item_autocomplete_matrix_item) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandItem.kt index 2bd0cffbe6..78cf24f114 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandItem.kt @@ -25,8 +25,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_autocomplete_command) -abstract class AutocompleteCommandItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class AutocompleteCommandItem : VectorEpoxyModel(R.layout.item_autocomplete_command) { @EpoxyAttribute var name: String? = null diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt index 26e2584929..036fbc2982 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -32,8 +32,8 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.features.reactions.data.EmojiItem import org.matrix.android.sdk.api.extensions.orFalse -@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) -abstract class AutocompleteEmojiItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class AutocompleteEmojiItem : VectorEpoxyModel(R.layout.item_autocomplete_emoji) { @EpoxyAttribute lateinit var emojiItem: EmojiItem diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteMoreResultItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteMoreResultItem.kt index 6925b15c9a..be7bd1adc2 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteMoreResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteMoreResultItem.kt @@ -21,8 +21,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result) -abstract class AutocompleteMoreResultItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class AutocompleteMoreResultItem : VectorEpoxyModel(R.layout.item_autocomplete_more_result) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 579a5851c6..9d7ada9d63 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -22,7 +22,6 @@ import android.app.PictureInPictureParams import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP -import android.content.res.Configuration import android.graphics.Color import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager @@ -35,8 +34,10 @@ import android.view.View import android.view.WindowManager import androidx.activity.result.ActivityResult import androidx.annotation.StringRes +import androidx.core.app.PictureInPictureModeChangedInfo import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import androidx.core.util.Consumer import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail @@ -50,6 +51,7 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.checkPermissions @@ -59,7 +61,7 @@ import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.transfer.CallTransferActivity import im.vector.app.features.call.utils.EglUtils -import im.vector.app.features.call.webrtc.ScreenCaptureService +import im.vector.app.features.call.webrtc.ScreenCaptureAndroidService import im.vector.app.features.call.webrtc.ScreenCaptureServiceConnection import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -94,7 +96,10 @@ data class CallArgs( private val loggerTag = LoggerTag("VectorCallActivity", LoggerTag.VOIP) @AndroidEntryPoint -class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionListener { +class VectorCallActivity : + VectorBaseActivity(), + CallControlsView.InteractionListener, + VectorMenuProvider { override fun getBinding() = ActivityCallBinding.inflate(layoutInflater) @@ -128,6 +133,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.BLACK super.onCreate(savedInstanceState) + addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}") if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) { @@ -210,25 +216,31 @@ class VectorCallActivity : VectorBaseActivity(), CallContro return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode } - override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) = withState(callViewModel) { - renderState(it) + private val pictureInPictureModeChangedInfoConsumer = Consumer { + withState(callViewModel) { + renderState(it) + } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.menu_call_open_chat) { - returnToChat() - return true - } else if (item.itemId == android.R.id.home) { - // We check here as we want PiP in some cases - onBackPressed() - return true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_call_open_chat -> { + returnToChat() + true + } + android.R.id.home -> { + // We check here as we want PiP in some cases + onBackPressed() + true + } + else -> false } - return super.onOptionsItemSelected(item) } override fun onDestroy() { detachRenderersIfNeeded() turnScreenOffAndKeyguardOn() + removeOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) super.onDestroy() } @@ -663,7 +675,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun startScreenSharingService(activityResult: ActivityResult) { ContextCompat.startForegroundService( this, - Intent(this, ScreenCaptureService::class.java) + Intent(this, ScreenCaptureAndroidService::class.java) ) bindToScreenCaptureService(activityResult) } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 86136eab15..5bf05d353c 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -19,12 +19,13 @@ package im.vector.app.features.call.conference import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import android.widget.Toast +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import com.airbnb.mvrx.Fail @@ -66,6 +67,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) jitsiViewModel.onEach { renderState(it) @@ -109,6 +111,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee ConferenceEventEmitter(this).emitConferenceEnded() } JitsiMeetActivityDelegate.onHostDestroy(this) + removeOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) super.onDestroy() } @@ -138,13 +141,9 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee .show() } - override fun onPictureInPictureModeChanged( - isInPictureInPictureMode: Boolean, - newConfig: Configuration - ) { - super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + private val pictureInPictureModeChangedInfoConsumer = Consumer { checkIfActivityShouldBeFinished() - Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)") + Timber.w("onPictureInPictureModeChanged(${it.isInPictureInPictureMode})") } private fun checkIfActivityShouldBeFinished() { diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionAndroidService.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt rename to vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionAndroidService.kt index 4a630dc451..f2f2209c35 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionAndroidService.kt @@ -29,7 +29,7 @@ import android.telecom.PhoneAccountHandle import android.telecom.StatusHints import android.telecom.TelecomManager import androidx.annotation.RequiresApi -import im.vector.app.core.services.CallService +import im.vector.app.core.services.CallAndroidService /** * No active calls in other apps @@ -47,7 +47,7 @@ import im.vector.app.core.services.CallService * the parameter followed by a call to the destroy() method if the user rejects the incoming call. * */ -@RequiresApi(Build.VERSION_CODES.M) class VectorConnectionService : ConnectionService() { +@RequiresApi(Build.VERSION_CODES.M) class VectorConnectionAndroidService : ConnectionService() { /** * The telecom subsystem calls this method in response to your app calling placeCall(Uri, Bundle) to create a new outgoing call. @@ -69,14 +69,14 @@ import im.vector.app.core.services.CallService connection.setCallerDisplayName("Element Caller", TelecomManager.PRESENTATION_ALLOWED) connection.statusHints = StatusHints("Testing Hint...", null, null) - bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0) + bindService(Intent(applicationContext, CallAndroidService::class.java), CallServiceConnection(connection), 0) connection.setInitializing() return connection } inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - val callSrvBinder = binder as CallService.CallServiceBinder + val callSrvBinder = binder as CallAndroidService.CallServiceBinder callSrvBinder.getCallService().addConnection(callConnection) unbindService(this) } 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 2bb544bdbb..3ec8f61978 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 @@ -49,6 +49,7 @@ class CallTransferPagerAdapter( fragment.arguments = UserListFragmentArgs( title = "", menuResId = -1, + submitMenuItemId = -1, singleSelection = true, showInviteActions = false, showToolbar = false, diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt similarity index 88% rename from vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt rename to vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt index 489b2d1eae..e7cebfb9c9 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt @@ -20,13 +20,13 @@ import android.content.Intent import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.core.services.VectorService +import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils import javax.inject.Inject @AndroidEntryPoint -class ScreenCaptureService : VectorService() { +class ScreenCaptureAndroidService : VectorAndroidService() { @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var clock: Clock @@ -53,6 +53,6 @@ class ScreenCaptureService : VectorService() { } inner class LocalBinder : Binder() { - fun getService(): ScreenCaptureService = this@ScreenCaptureService + fun getService(): ScreenCaptureAndroidService = this@ScreenCaptureAndroidService } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt index aa7c7f450a..f55a3559bd 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt @@ -32,7 +32,7 @@ class ScreenCaptureServiceConnection @Inject constructor( } private var isBound = false - private var screenCaptureService: ScreenCaptureService? = null + private var screenCaptureAndroidService: ScreenCaptureAndroidService? = null private var callback: Callback? = null fun bind(callback: Callback) { @@ -41,25 +41,25 @@ class ScreenCaptureServiceConnection @Inject constructor( if (isBound) { callback.onServiceConnected() } else { - Intent(context, ScreenCaptureService::class.java).also { intent -> + Intent(context, ScreenCaptureAndroidService::class.java).also { intent -> context.bindService(intent, this, 0) } } } fun stopScreenCapturing() { - screenCaptureService?.stopService() + screenCaptureAndroidService?.stopService() } override fun onServiceConnected(className: ComponentName, binder: IBinder) { - screenCaptureService = (binder as ScreenCaptureService.LocalBinder).getService() + screenCaptureAndroidService = (binder as ScreenCaptureAndroidService.LocalBinder).getService() isBound = true callback?.onServiceConnected() } override fun onServiceDisconnected(className: ComponentName) { isBound = false - screenCaptureService = null + screenCaptureAndroidService = null callback = null } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 79c3930c89..00b9a76de7 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -19,7 +19,7 @@ package im.vector.app.features.call.webrtc import android.content.Context import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService -import im.vector.app.core.services.CallService +import im.vector.app.core.services.CallAndroidService import im.vector.app.core.utils.PublishDataSource import im.vector.app.core.utils.TextUtils.formatDuration import im.vector.app.features.call.CameraEventsHandlerAdapter @@ -477,7 +477,7 @@ class WebRtcCall( val turnServerResponse = getTurnServer() // Update service state withContext(Dispatchers.Main) { - CallService.onPendingCall( + CallAndroidService.onPendingCall( context = context, callId = mxCall.callId ) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index db03e7dc5d..b35ab774be 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.LifecycleOwner import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.core.pushers.UnifiedPushHelper -import im.vector.app.core.services.CallService +import im.vector.app.core.services.CallAndroidService import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CallEnded import im.vector.app.features.analytics.plan.CallStarted @@ -254,7 +254,7 @@ class WebRtcCallManager @Inject constructor( Timber.tag(loggerTag.value).v("On call ended for unknown call $callId") } webRtcCall.trackCallEnded() - CallService.onCallTerminated(context, callId, endCallReason, rejected) + CallAndroidService.onCallTerminated(context, callId, endCallReason, rejected) callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) transferees.remove(callId) @@ -305,7 +305,7 @@ class WebRtcCallManager @Inject constructor( if (transferee != null) { transferees[webRtcCall.callId] = transferee } - CallService.onOutgoingCallRinging( + CallAndroidService.onOutgoingCallRinging( context = context.applicationContext, callId = mxCall.callId ) @@ -370,7 +370,7 @@ class WebRtcCallManager @Inject constructor( offerSdp = callInviteContent.offer } // Start background service with notification - CallService.onIncomingCallRinging( + CallAndroidService.onIncomingCallRinging( context = context, callId = mxCall.callId, isInBackground = isInBackground @@ -395,7 +395,7 @@ class WebRtcCallManager @Inject constructor( } val mxCall = call.mxCall // Update service state - CallService.onPendingCall( + CallAndroidService.onPendingCall( context = context, callId = mxCall.callId ) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactDetailItem.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactDetailItem.kt index fbf404b74d..434c2f5c44 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactDetailItem.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactDetailItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_contact_detail) -abstract class ContactDetailItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ContactDetailItem : VectorEpoxyModel(R.layout.item_contact_detail) { @EpoxyAttribute lateinit var threePid: String @EpoxyAttribute var matrixId: String? = null diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactItem.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactItem.kt index fd112f0948..cd5322c4f8 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactItem.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_contact_main) -abstract class ContactItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ContactItem : VectorEpoxyModel(R.layout.item_contact_main) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var mappedContact: MappedContact diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt index 83c7f0a13b..b5657598ee 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt @@ -20,10 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.userdirectory.PendingSelection sealed class CreateDirectRoomAction : VectorViewModelAction { - data class CreateRoomAndInviteSelectedUsers( + data class PrepareRoomWithSelectedUsers( val selections: Set ) : CreateDirectRoomAction() + object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() + data class QrScannedAction( val result: String ) : CreateDirectRoomAction() diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 6292217b67..707b78d328 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -81,7 +81,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { when (action) { UserListSharedAction.Close -> finish() UserListSharedAction.GoBack -> onBackPressed() - is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action) + is UserListSharedAction.OnMenuItemSubmitClick -> handleOnMenuItemSubmitClick(action) UserListSharedAction.OpenPhoneBook -> openPhoneBook() UserListSharedAction.AddByQrCode -> openAddByQrCode() } @@ -93,7 +93,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { UserListFragment::class.java, UserListFragmentArgs( title = getString(R.string.fab_menu_create_chat), - menuResId = R.menu.vector_create_direct_room + menuResId = R.menu.vector_create_direct_room, + submitMenuItemId = R.id.action_create_direct_room, ) ) } @@ -159,10 +160,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } } - private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { - if (action.itemId == R.id.action_create_direct_room) { - viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selections)) - } + private fun handleOnMenuItemSubmitClick(action: UserListSharedAction.OnMenuItemSubmitClick) { + viewModel.handle(CreateDirectRoomAction.PrepareRoomWithSelectedUsers(action.selections)) } private fun renderCreateAndInviteState(state: Async) { 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 8374f9d513..b306cb6e03 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 @@ -26,6 +26,7 @@ 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 @@ -46,7 +47,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor( @Assisted initialState: CreateDirectRoomViewState, private val rawService: RawService, val session: Session, - val analyticsTracker: AnalyticsTracker + val analyticsTracker: AnalyticsTracker, + val vectorFeatures: VectorFeatures ) : VectorViewModel(initialState) { @@ -59,7 +61,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor( override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections) + is CreateDirectRoomAction.PrepareRoomWithSelectedUsers -> onSubmitInvitees(action.selections) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onCreateRoomWithInvitees() is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action) } } @@ -94,16 +97,18 @@ class CreateDirectRoomViewModel @AssistedInject constructor( } if (existingRoomId != null) { // Do not create a new DM, just tell that the creation is successful by passing the existing roomId - setState { - copy(createAndInviteState = Success(existingRoomId)) - } + setState { copy(createAndInviteState = Success(existingRoomId)) } } else { - // Create the DM - createRoomAndInviteSelectedUsers(selections) + createLocalRoomWithSelectedUsers(selections) } } - private fun createRoomAndInviteSelectedUsers(selections: Set) { + private fun onCreateRoomWithInvitees() { + // Create the DM + withState { createLocalRoomWithSelectedUsers(it.pendingSelections) } + } + + private fun createLocalRoomWithSelectedUsers(selections: Set) { setState { copy(createAndInviteState = Loading()) } viewModelScope.launch(Dispatchers.IO) { @@ -124,7 +129,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor( } val result = runCatchingToAsync { - session.roomService().createRoom(roomParams) + if (vectorFeatures.shouldStartDmOnFirstMessage()) { + session.roomService().createLocalRoom(roomParams) + } else { + session.roomService().createRoom(roomParams) + } } analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse())) diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt index 41366a7110..33360ac20c 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt @@ -19,7 +19,9 @@ package im.vector.app.features.createdirect import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.userdirectory.PendingSelection data class CreateDirectRoomViewState( + val pendingSelections: Set = emptySet(), val createAndInviteState: Async = Uninitialized ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingFooterItem.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingFooterItem.kt index 69551cac93..cbca0c99eb 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingFooterItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingFooterItem.kt @@ -27,8 +27,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_keys_backup_settings_button_footer) -abstract class KeysBackupSettingFooterItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class KeysBackupSettingFooterItem : VectorEpoxyModel(R.layout.item_keys_backup_settings_button_footer) { @EpoxyAttribute var textButton1: String? = null diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 7478adb165..077bcc2cf3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -187,16 +187,6 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } } -// I think this code is useful, but it violates the code quality rules -// override fun onOptionsItemSelected(item: MenuItem): Boolean { -// if (item.itemId == android .R. id. home) { -// onBackPressed() -// return true -// } -// -// return super.onOptionsItemSelected(item) -// } - companion object { const val KEYS_VERSION = "KEYS_VERSION" const val MANUAL_EXPORT = "MANUAL_EXPORT" diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetSelfWaitItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetSelfWaitItem.kt index 205dd6a10a..1f4a9f254b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetSelfWaitItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetSelfWaitItem.kt @@ -24,7 +24,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_wait) -abstract class BottomSheetSelfWaitItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetSelfWaitItem : VectorEpoxyModel(R.layout.item_verification_wait) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt index 7dc7a31441..7a0d5ce69d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt @@ -34,8 +34,8 @@ import im.vector.app.core.extensions.setTextOrHide /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_action) -abstract class BottomSheetVerificationActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationActionItem : VectorEpoxyModel(R.layout.item_verification_action) { @EpoxyAttribute @DrawableRes diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt index adf3e8f7e5..2fd88f7bca 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt @@ -27,8 +27,8 @@ import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_big_image) -abstract class BottomSheetVerificationBigImageItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationBigImageItem : VectorEpoxyModel(R.layout.item_verification_big_image) { @EpoxyAttribute lateinit var roomEncryptionTrustLevel: RoomEncryptionTrustLevel diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt index c8afc88769..052ad3b4bc 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt @@ -26,8 +26,10 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_decimal_code) -abstract class BottomSheetVerificationDecimalCodeItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationDecimalCodeItem : VectorEpoxyModel( + R.layout.item_verification_decimal_code +) { @EpoxyAttribute var code: String = "" diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt index e1b703e0fc..b591e27296 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt @@ -34,8 +34,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentatio /** * A emoji list for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_emojis) -abstract class BottomSheetVerificationEmojisItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationEmojisItem : VectorEpoxyModel(R.layout.item_verification_emojis) { @EpoxyAttribute lateinit var emojiRepresentation0: EmojiRepresentation @EpoxyAttribute lateinit var emojiRepresentation1: EmojiRepresentation diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt index ecd9989cdc..07f04b8ee2 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt @@ -27,8 +27,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_notice) -abstract class BottomSheetVerificationNoticeItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationNoticeItem : VectorEpoxyModel(R.layout.item_verification_notice) { @EpoxyAttribute lateinit var notice: EpoxyCharSequence diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt index 0041631986..8f20160108 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.ui.views.QrCodeImageView /** * An Epoxy item displaying a QR code. */ -@EpoxyModelClass(layout = R.layout.item_verification_qr_code) -abstract class BottomSheetVerificationQrCodeItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationQrCodeItem : VectorEpoxyModel(R.layout.item_verification_qr_code) { @EpoxyAttribute lateinit var data: String diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt index 46a1dd04a8..8c784cc7ac 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel /** * A action for bottom sheet. */ -@EpoxyModelClass(layout = R.layout.item_verification_waiting) -abstract class BottomSheetVerificationWaitingItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BottomSheetVerificationWaitingItem : VectorEpoxyModel(R.layout.item_verification_waiting) { @EpoxyAttribute var title: String = "" diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt index 6f661c5164..774460eb1f 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt @@ -21,7 +21,6 @@ import android.content.Intent import android.os.Parcelable import android.view.Menu import android.view.MenuItem -import androidx.core.view.forEach import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Fail @@ -36,6 +35,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.createJSonViewerStyleProvider import kotlinx.parcelize.Parcelize @@ -43,7 +43,10 @@ import org.billcarsonfr.jsonviewer.JSonViewerFragment import javax.inject.Inject @AndroidEntryPoint -class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStackChangedListener { +class RoomDevToolActivity : + SimpleFragmentActivity(), + FragmentManager.OnBackStackChangedListener, + VectorMenuProvider { @Inject lateinit var colorProvider: ColorProvider @@ -133,16 +136,18 @@ class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStac } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.menuItemEdit) { - viewModel.handle(RoomDevToolAction.MenuEdit) - return true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menuItemEdit -> { + viewModel.handle(RoomDevToolAction.MenuEdit) + true + } + R.id.menuItemSend -> { + viewModel.handle(RoomDevToolAction.MenuItemSend) + true + } + else -> false } - if (item.itemId == R.id.menuItemSend) { - viewModel.handle(RoomDevToolAction.MenuItemSend) - return true - } - return super.onOptionsItemSelected(item) } override fun onBackPressed() { @@ -174,21 +179,12 @@ class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStac super.onDestroy() } - override fun onPrepareOptionsMenu(menu: Menu?): Boolean = withState(viewModel) { state -> - menu?.forEach { - val isVisible = when (it.itemId) { - R.id.menuItemEdit -> { - state.displayMode is RoomDevToolViewState.Mode.StateEventDetail - } - R.id.menuItemSend -> { - state.displayMode is RoomDevToolViewState.Mode.EditEventContent || - state.displayMode is RoomDevToolViewState.Mode.SendEventForm - } - else -> true - } - it.isVisible = isVisible + override fun handlePrepareMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.menuItemEdit).isVisible = state.displayMode == RoomDevToolViewState.Mode.StateEventDetail + menu.findItem(R.id.menuItemSend).isVisible = state.displayMode == RoomDevToolViewState.Mode.EditEventContent || + state.displayMode is RoomDevToolViewState.Mode.SendEventForm } - return@withState true } companion object { diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt index 4df4146d2f..ef8c43aab4 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt @@ -19,15 +19,15 @@ package im.vector.app.features.discovery import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_discovery_policy) -abstract class DiscoveryPolicyItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class DiscoveryPolicyItem : VectorEpoxyModel(R.layout.item_discovery_policy) { @EpoxyAttribute var name: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsButtonItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsButtonItem.kt index c025779339..da432c43b1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsButtonItem.kt @@ -19,17 +19,17 @@ import android.widget.Button import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.attributes.ButtonStyle import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.resources.ColorProvider -@EpoxyModelClass(layout = R.layout.item_settings_button) -abstract class SettingsButtonItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsButtonItem : VectorEpoxyModel(R.layout.item_settings_button) { @EpoxyAttribute lateinit var colorProvider: ColorProvider diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsCenteredImageItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsCenteredImageItem.kt index af79dd8bb5..c8dd9fe304 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsCenteredImageItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsCenteredImageItem.kt @@ -19,12 +19,12 @@ import android.widget.ImageView import androidx.annotation.DrawableRes import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_settings_centered_image) -abstract class SettingsCenteredImageItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsCenteredImageItem : VectorEpoxyModel(R.layout.item_settings_centered_image) { @EpoxyAttribute @DrawableRes diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index 1f19523202..cbb8794636 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -18,14 +18,14 @@ package im.vector.app.features.discovery import android.widget.Button import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_settings_continue_cancel) -abstract class SettingsContinueCancelItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsContinueCancelItem : VectorEpoxyModel(R.layout.item_settings_continue_cancel) { @EpoxyAttribute var continueText: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt index ad139309ac..ea2e51da24 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsEditTextItem.kt @@ -22,15 +22,15 @@ import android.widget.TextView import androidx.core.widget.doOnTextChanged import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showKeyboard -@EpoxyModelClass(layout = R.layout.item_settings_edit_text) -abstract class SettingsEditTextItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsEditTextItem : VectorEpoxyModel(R.layout.item_settings_edit_text) { @EpoxyAttribute var hint: String? = null @EpoxyAttribute var value: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsInfoItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsInfoItem.kt index ea5f044274..b4b1a112ba 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsInfoItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsInfoItem.kt @@ -20,15 +20,15 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_settings_helper_info) -abstract class SettingsInfoItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsInfoItem : VectorEpoxyModel(R.layout.item_settings_helper_info) { @EpoxyAttribute var helperText: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsInformationItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsInformationItem.kt index 71eab5115e..aa98cfbe69 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsInformationItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsInformationItem.kt @@ -19,12 +19,12 @@ import android.widget.TextView import androidx.annotation.ColorInt import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_settings_information) -abstract class SettingsInformationItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsInformationItem : VectorEpoxyModel(R.layout.item_settings_information) { @EpoxyAttribute lateinit var message: String diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt index 1a14b4c9f3..4d117067e6 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsItem.kt @@ -20,16 +20,16 @@ import androidx.annotation.StringRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import com.google.android.material.switchmaterial.SwitchMaterial import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_settings_simple_item) -abstract class SettingsItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsItem : VectorEpoxyModel(R.layout.item_settings_simple_item) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsProgressItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsProgressItem.kt index 8d62b37972..657ce9b10b 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsProgressItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsProgressItem.kt @@ -16,12 +16,12 @@ package im.vector.app.features.discovery import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_settings_progress) -abstract class SettingsProgressItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsProgressItem : VectorEpoxyModel(R.layout.item_settings_progress) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsSectionTitleItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsSectionTitleItem.kt index fe97984c8b..7e3e156d58 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsSectionTitleItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsSectionTitleItem.kt @@ -19,13 +19,13 @@ import android.widget.TextView import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_settings_section_title) -abstract class SettingsSectionTitleItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsSectionTitleItem : VectorEpoxyModel(R.layout.item_settings_section_title) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt index 192ee404fb..9810ae2400 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsTextButtonSingleLineItem.kt @@ -25,11 +25,11 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import com.google.android.material.switchmaterial.SwitchMaterial import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.attributes.ButtonStyle import im.vector.app.core.epoxy.attributes.ButtonType import im.vector.app.core.epoxy.attributes.IconMode @@ -39,8 +39,8 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.themes.ThemeUtils -@EpoxyModelClass(layout = R.layout.item_settings_button_single_line) -abstract class SettingsTextButtonSingleLineItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class SettingsTextButtonSingleLineItem : VectorEpoxyModel(R.layout.item_settings_button_single_line) { @EpoxyAttribute lateinit var colorProvider: ColorProvider diff --git a/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt index d50b429c97..6f2aa2086b 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormAdvancedToggleItem.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.features.themes.ThemeUtils -@EpoxyModelClass(layout = R.layout.item_form_advanced_toggle) -abstract class FormAdvancedToggleItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormAdvancedToggleItem : VectorEpoxyModel(R.layout.item_form_advanced_toggle) { @EpoxyAttribute lateinit var title: String @EpoxyAttribute var expanded: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index c58b36fc88..9f32980874 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -34,8 +34,8 @@ import im.vector.app.core.epoxy.addTextChangedListenerOnce import im.vector.app.core.epoxy.setValueOnce import im.vector.app.core.platform.SimpleTextWatcher -@EpoxyModelClass(layout = R.layout.item_form_text_input) -abstract class FormEditTextItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormEditTextItem : VectorEpoxyModel(R.layout.item_form_text_input) { @EpoxyAttribute var hint: String? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt index dd059ec658..011446a020 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt @@ -32,8 +32,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.setValueOnce import im.vector.app.core.platform.SimpleTextWatcher -@EpoxyModelClass(layout = R.layout.item_form_text_input_with_button) -abstract class FormEditTextWithButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormEditTextWithButtonItem : VectorEpoxyModel(R.layout.item_form_text_input_with_button) { @EpoxyAttribute var hint: String? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt index 637955a66c..64feca87f7 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithDeleteItem.kt @@ -34,8 +34,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.core.platform.SimpleTextWatcher -@EpoxyModelClass(layout = R.layout.item_form_text_input_with_delete) -abstract class FormEditTextWithDeleteItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormEditTextWithDeleteItem : VectorEpoxyModel(R.layout.item_form_text_input_with_delete) { @EpoxyAttribute var hint: String? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt index b81a731260..75b9379903 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt @@ -21,18 +21,18 @@ import android.widget.ImageView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import com.bumptech.glide.request.RequestOptions import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_editable_avatar) -abstract class FormEditableAvatarItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class FormEditableAvatarItem : VectorEpoxyModel(R.layout.item_editable_avatar) { @EpoxyAttribute var avatarRenderer: AvatarRenderer? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt index a25050cad0..1533f8f22c 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt @@ -22,20 +22,20 @@ import android.widget.ImageView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import com.bumptech.glide.load.MultiTransformation import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_editable_square_avatar) -abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class FormEditableSquareAvatarItem : VectorEpoxyModel(R.layout.item_editable_square_avatar) { @EpoxyAttribute var avatarRenderer: AvatarRenderer? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt index 0753248272..7080f6d001 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormMultiLineEditTextItem.kt @@ -30,8 +30,8 @@ import im.vector.app.core.epoxy.addTextChangedListenerOnce import im.vector.app.core.epoxy.setValueOnce import im.vector.app.core.platform.SimpleTextWatcher -@EpoxyModelClass(layout = R.layout.item_form_multiline_text_input) -abstract class FormMultiLineEditTextItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormMultiLineEditTextItem : VectorEpoxyModel(R.layout.item_form_multiline_text_input) { @EpoxyAttribute var hint: String? = null diff --git a/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt index 41e1bc95a7..d425111fec 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt @@ -19,15 +19,15 @@ import android.widget.Button import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_form_submit_button) -abstract class FormSubmitButtonItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class FormSubmitButtonItem : VectorEpoxyModel(R.layout.item_form_submit_button) { @EpoxyAttribute var enabled: Boolean = true diff --git a/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt index 800a90135b..00d1738aaf 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.setValueOnce import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_form_switch) -abstract class FormSwitchItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FormSwitchItem : VectorEpoxyModel(R.layout.item_form_switch) { @EpoxyAttribute var listener: ((Boolean) -> Unit)? = null diff --git a/vector/src/main/java/im/vector/app/features/grouplist/GroupSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/GroupSummaryItem.kt index 4256e2d808..ce2b35b353 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/GroupSummaryItem.kt @@ -30,8 +30,8 @@ import im.vector.app.core.platform.CheckableConstraintLayout import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_group) -abstract class GroupSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class GroupSummaryItem : VectorEpoxyModel(R.layout.item_group) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt index b7520268cb..e1b33f9460 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -34,8 +34,8 @@ import im.vector.app.core.platform.CheckableConstraintLayout import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.themes.ThemeUtils -@EpoxyModelClass(layout = R.layout.item_space) -abstract class HomeSpaceSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class HomeSpaceSummaryItem : VectorEpoxyModel(R.layout.item_space) { @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null 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 9cf2048444..eb615466ed 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 @@ -45,6 +45,7 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.resources.ColorProvider @@ -110,7 +111,8 @@ class HomeActivity : VectorBaseActivity(), NavigationInterceptor, SpaceInviteBottomSheet.InteractionListener, - MatrixToBottomSheet.InteractionListener { + MatrixToBottomSheet.InteractionListener, + VectorMenuProvider { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -563,7 +565,7 @@ class HomeActivity : override fun getMenuRes() = R.menu.home - override fun onPrepareOptionsMenu(menu: Menu): Boolean { + override fun handlePrepareMenu(menu: Menu) { menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode() menu.findItem(R.id.menu_home_init_sync_optimized)?.isVisible = vectorPreferences.developerMode() menu.findItem(R.id.dev_theming)?.isVisible = vectorPreferences.developerMode() @@ -597,50 +599,49 @@ class HomeActivity : restart() true } - - return super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + val handled = when (item.itemId) { /* R.id.menu_home_suggestion -> { bugReporter.openBugReportScreen(this, ReportType.SUGGESTION) - return true + true } */ R.id.menu_home_report_bug -> { bugReporter.openBugReportScreen(this, ReportType.BUG_REPORT) - return true + true } R.id.menu_home_init_sync_legacy -> { // Configure the SDK initialSyncStrategy = InitialSyncStrategy.Legacy // And clear cache MainActivity.restartApp(this, MainActivityArgs(clearCache = true)) - return true + true } R.id.menu_home_init_sync_optimized -> { // Configure the SDK initialSyncStrategy = InitialSyncStrategy.Optimized() // And clear cache MainActivity.restartApp(this, MainActivityArgs(clearCache = true)) - return true + true } R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) - return true + true } R.id.menu_home_setting -> { navigator.openSettings(this) - return true + true } + else -> false } - return ArrayOptionsMenuHelper.handleSubmenu(item, + return handled || ArrayOptionsMenuHelper.handleSubmenu(item, R.id.dev_base_theme, R.id.dev_theme_accent - ) || super.onOptionsItemSelected(item) + ) } override fun onBackPressed() { 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 29391af7d7..f110f0f11e 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 @@ -45,6 +45,7 @@ import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseActivity 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.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsViewPresenter @@ -83,7 +84,8 @@ class HomeDetailFragment @Inject constructor( ) : VectorBaseFragment(), KeysBackupBanner.Delegate, CurrentCallsView.Callback, - OnBackPressed { + OnBackPressed, + VectorMenuProvider { private val DEBUG_VIEW_PAGER = DbgUtil.isDbgEnabled(DbgUtil.DBG_VIEW_PAGER) private val viewPagerDimber = Dimber("Home pager", DbgUtil.DBG_VIEW_PAGER) @@ -116,23 +118,21 @@ class HomeDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.room_list - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.menu_home_mark_all_as_read -> { viewModel.handle(HomeDetailAction.MarkAllRoomsRead) - return true + true } + else -> false } - - return super.onOptionsItemSelected(item) } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { withState(viewModel) { state -> val isRoomList = state.currentTab is HomeTab.RoomList menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms } - super.onPrepareOptionsMenu(menu) } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt index 4ab049aad8..f72597f268 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -32,8 +32,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_breadcrumbs) -abstract class BreadcrumbsItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BreadcrumbsItem : VectorEpoxyModel(R.layout.item_breadcrumbs) { @EpoxyAttribute var hasTypingUsers: Boolean = false @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 5751137390..c203c86391 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState 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.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.sync.SyncRequestState @@ -112,4 +113,6 @@ data class RoomDetailViewState( fun isPublic() = asyncRoomSummary()?.isPublic == true fun isThreadTimeline() = rootThreadEventId != null + + fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId) } 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 151582fbc1..1b4b6152e3 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 @@ -32,7 +32,6 @@ import android.view.HapticFeedbackConstants import android.view.KeyEvent import android.view.LayoutInflater import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -101,6 +100,7 @@ import im.vector.app.core.hardware.vibrate import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider @@ -305,7 +305,8 @@ class TimelineFragment @Inject constructor( AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, GalleryOrCameraDialogHelper.Listener, - CurrentCallsView.Callback { + CurrentCallsView.Callback, + VectorMenuProvider { private val rmDimber = Dimber("ReadMarkerDbg", DbgUtil.DBG_READ_MARKER) @@ -1118,15 +1119,14 @@ class TimelineFragment @Inject constructor( } @SuppressLint("RestrictedApi") - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + override fun handlePostCreateMenu(menu: Menu) { if (isThreadTimeLine()) { if (menu is MenuBuilder) menu.setOptionalIconsVisible(true) } - super.onCreateOptionsMenu(menu, inflater) // We use a custom layout for this menu item, so we need to set a ClickListener menu.findItem(R.id.open_matrix_apps)?.let { menuItem -> menuItem.actionView.debouncedClicks { - onOptionsItemSelected(menuItem) + handleMenuItemSelected(menuItem) } } val joinConfItem = menu.findItem(R.id.join_conference) @@ -1136,13 +1136,13 @@ class TimelineFragment @Inject constructor( // Custom thread notification menu item menu.findItem(R.id.menu_timeline_thread_list)?.let { menuItem -> - menuItem.actionView.setOnClickListener { - onOptionsItemSelected(menuItem) + menuItem.actionView.debouncedClicks { + handleMenuItemSelected(menuItem) } } } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { menu.forEach { it.isVisible = timelineViewModel.isMenuItemVisible(it.itemId) } @@ -1250,7 +1250,7 @@ class TimelineFragment @Inject constructor( } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.invite -> { navigator.openInviteUsersToRoom(requireActivity(), timelineArgs.roomId) @@ -1348,7 +1348,7 @@ class TimelineFragment @Inject constructor( R.id.dev_bubble_style, R.id.dev_theme_accent, R.id.dev_bubble_rounded_corners - ) || super.onOptionsItemSelected(item) + ) } } @@ -1444,6 +1444,7 @@ class TimelineFragment @Inject constructor( val nonFormattedBody = when (messageContent) { is MessageAudioContent -> getAudioContentBodyText(messageContent) is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() + is MessageBeaconInfoContent -> getString(R.string.sent_live_location) else -> messageContent?.body.orEmpty() } var formattedBody: CharSequence? = null @@ -1679,6 +1680,9 @@ class TimelineFragment @Inject constructor( updateJumpToReadMarkerViewVisibility() jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } + }.apply { + // For local rooms, pin the view's content to the top edge (the layout is reversed) + stackFromEnd = isLocalRoom() } val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController, views.timelineRecyclerView) @@ -1893,7 +1897,7 @@ class TimelineFragment @Inject constructor( private fun sendUri(uri: Uri): Boolean { val shareIntent = Intent(Intent.ACTION_SEND, uri) - val isHandled = shareIntentHandler.handleIncomingShareIntent(requireContext(), shareIntent, ::onContentAttachmentsReady, onPlainText = { + val isHandled = shareIntentHandler.handleIncomingShareIntent(shareIntent, ::onContentAttachmentsReady, onPlainText = { fatalError("Should not happen as we're generating a File based share Intent", vectorPreferences.failFast()) }) if (!isHandled) { @@ -1994,32 +1998,42 @@ class TimelineFragment @Inject constructor( } private fun renderToolbar(roomSummary: RoomSummary?) { - if (!isThreadTimeLine()) { - views.includeRoomToolbar.roomToolbarContentView.isVisible = true - views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false - if (roomSummary == null) { - views.includeRoomToolbar.roomToolbarContentView.isClickable = false - } else { - views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN - views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName - avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) - val showPresence = roomSummary.isDirect - views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence) - //val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield - val shieldView = views.includeRoomToolbar.roomToolbarDecorationImageView - shieldView.render(roomSummary.roomEncryptionTrustLevel) - views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + when { + isLocalRoom() -> { + views.includeRoomToolbar.roomToolbarContentView.isVisible = false + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false + setupToolbar(views.roomToolbar) + .setTitle(R.string.room_member_open_or_create_dm) + .allowBack(useCross = true) } - } else { - views.includeRoomToolbar.roomToolbarContentView.isVisible = false - views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true - timelineArgs.threadTimelineArgs?.let { - val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) - avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) - views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel) - views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName + isThreadTimeLine() -> { + views.includeRoomToolbar.roomToolbarContentView.isVisible = false + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true + timelineArgs.threadTimelineArgs?.let { + val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) + views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel) + views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName + } + views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) + } + else -> { + views.includeRoomToolbar.roomToolbarContentView.isVisible = true + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false + if (roomSummary == null) { + views.includeRoomToolbar.roomToolbarContentView.isClickable = false + } else { + views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN + views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName + avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) + val showPresence = roomSummary.isDirect + views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence) + //val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield + val shieldView = views.includeRoomToolbar.roomToolbarDecorationImageView + shieldView.render(roomSummary.roomEncryptionTrustLevel) + views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + } } - views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } } @@ -3015,6 +3029,11 @@ class TimelineFragment @Inject constructor( */ private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null + /** + * Returns true if the current room is a local room, false otherwise. + */ + private fun isLocalRoom(): Boolean = withState(timelineViewModel) { it.isLocalRoom() } + /** * Returns the root thread event if we are in a thread room, otherwise returns null. */ 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 78c2bc64b8..ed78fa3164 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 @@ -778,34 +778,39 @@ class TimelineViewModel @AssistedInject constructor( return@withState false } - if (initialState.isThreadTimeline()) { - when (itemId) { - R.id.menu_thread_timeline_view_in_room, - R.id.menu_thread_timeline_copy_link, - R.id.menu_thread_timeline_share -> true - else -> false + when { + initialState.isLocalRoom() -> false + initialState.isThreadTimeline() -> { + when (itemId) { + R.id.menu_thread_timeline_view_in_room, + R.id.menu_thread_timeline_copy_link, + R.id.menu_thread_timeline_share -> true + else -> false + } } - } else { - when (itemId) { - R.id.timeline_setting -> false // replaced by show_room_info (downstream) - R.id.invite -> false // state.canInvite // SC: disabled, we can do that over show_participants as well - R.id.open_matrix_apps -> session.integrationManagerService().isIntegrationEnabled() - R.id.voice_call -> !vectorPreferences.hideCallButtons() && state.isCallOptionAvailable() - R.id.video_call -> !vectorPreferences.hideCallButtons() && (state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined) - // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ - R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> state.isSearchAvailable() - R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled() - R.id.show_room_info -> true // SC - R.id.show_participants -> true // SC - // SC dev start - R.id.dev_bubble_style, - R.id.dev_hidden_events, - R.id.dev_event_visibilities, - R.id.dev_theming, - // SC dev end - R.id.dev_tools -> vectorPreferences.developerMode() - else -> false + else -> { + when (itemId) { + R.id.timeline_setting -> false // replaced by show_room_info (downstream) + R.id.invite -> false // state.canInvite // SC: disabled, we can do that over show_participants as well + R.id.open_matrix_apps -> session.integrationManagerService().isIntegrationEnabled() + R.id.voice_call -> !vectorPreferences.hideCallButtons() && state.isCallOptionAvailable() + R.id.video_call -> !vectorPreferences.hideCallButtons() && (state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined) + // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ + R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined + R.id.search -> state.isSearchAvailable() + R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled() + // SC extras start + R.id.show_room_info -> true // SC + R.id.show_participants -> true // SC + // SC dev start + R.id.dev_bubble_style, + R.id.dev_hidden_events, + R.id.dev_event_visibilities, + R.id.dev_theming, + // SC dev end + R.id.dev_tools -> vectorPreferences.developerMode() + else -> false + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt index 9e3cbef1cc..a489c1ec66 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -21,17 +21,17 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +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.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_display_read_receipt) -abstract class DisplayReadReceiptItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class DisplayReadReceiptItem : VectorEpoxyModel(R.layout.item_display_read_receipt) { @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var timestamp: String? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index d92dcdd3ef..60cbf1311a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -34,8 +34,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.session.threads.ThreadDetails import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_search_result) -abstract class SearchResultItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SearchResultItem : VectorEpoxyModel(R.layout.item_search_result) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute var formattedDate: String? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 331ac3d2b1..022fe39e6f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -640,12 +640,13 @@ class TimelineEventController @Inject constructor( } private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean { - return if (hasReachedInvite && hasUTD) { - true - } else { - val date = event.root.localDateTime() - val nextDate = nextEvent?.root?.localDateTime() - date.toLocalDate() != nextDate?.toLocalDate() + return when { + hasReachedInvite && hasUTD -> true + else -> { + val date = event.root.localDateTime() + val nextDate = nextEvent?.root?.localDateTime() + date.toLocalDate() != nextDate?.toLocalDate() + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt new file mode 100644 index 0000000000..50533e2e31 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt @@ -0,0 +1,46 @@ +/* + * 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.detail.timeline.action + +import org.matrix.android.sdk.api.session.events.model.EventType +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.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class CheckIfCanReplyEventUseCase @Inject constructor() { + + fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { + // Only EventType.MESSAGE, EventType.STICKER, EventType.POLL_START and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment + if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO + EventType.POLL_START + EventType.MESSAGE + EventType.STICKER) return false + if (!actionPermissions.canSendMessage) return false + return when (messageContent?.msgType) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_STICKER_LOCAL, + MessageType.MSGTYPE_POLL_START, + MessageType.MSGTYPE_BEACON_INFO, + MessageType.MSGTYPE_LOCATION -> true + else -> false + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 1adf2ab782..69debfbd43 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -80,7 +80,8 @@ class MessageActionsViewModel @AssistedInject constructor( private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, - private val vectorPreferences: VectorPreferences + private val vectorPreferences: VectorPreferences, + private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase, ) : VectorViewModel(initialState) { private val informationData = initialState.informationData @@ -436,23 +437,7 @@ class MessageActionsViewModel @AssistedInject constructor( } private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { - // Only EventType.MESSAGE, EventType.STICKER and EventType.POLL_START event types are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false - - if (!actionPermissions.canSendMessage) return false - return when (messageContent?.msgType) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_FILE, - MessageType.MSGTYPE_STICKER_LOCAL, - MessageType.MSGTYPE_POLL_START, - MessageType.MSGTYPE_LOCATION -> true - else -> false - } + return checkIfCanReplyEventUseCase.execute(event, messageContent, actionPermissions) } /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index dd058197b4..5f32696334 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoomSummary +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import javax.inject.Inject class EncryptionItemFactory @Inject constructor( @@ -55,12 +56,19 @@ class EncryptionItemFactory @Inject constructor( val description: String val shield: StatusTileTimelineItem.ShieldUIState if (isSafeAlgorithm) { + val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse() title = stringProvider.getString(R.string.encryption_enabled) description = stringProvider.getString( - if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) { - R.string.direct_room_encryption_enabled_tile_description - } else { - R.string.encryption_enabled_tile_description + when { + isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> { + R.string.direct_room_encryption_enabled_tile_description_future + } + isDirect -> { + R.string.direct_room_encryption_enabled_tile_description + } + else -> { + R.string.encryption_enabled_tile_description + } } ) shield = StatusTileTimelineItem.ShieldUIState.BLACK diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 01034160a9..b2be8bb72c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -119,6 +119,7 @@ class MergedHeaderItemFactory @Inject constructor( highlighted = true } val data = BasedMergedItem.Data( + roomId = mergedEvent.root.roomId, userId = mergedEvent.root.senderId ?: "", avatarUrl = mergedEvent.senderInfo.avatarUrl, memberName = mergedEvent.senderInfo.disambiguatedDisplayName, @@ -202,6 +203,7 @@ class MergedHeaderItemFactory @Inject constructor( highlighted = true } val data = BasedMergedItem.Data( + roomId = mergedEvent.root.roomId, userId = mergedEvent.root.senderId ?: "", avatarUrl = mergedEvent.senderInfo.avatarUrl, memberName = mergedEvent.senderInfo.disambiguatedDisplayName, 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 9f95e73925..167b4b3a22 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 @@ -249,7 +249,7 @@ class MessageItemFactory @Inject constructor( .eventId(informationData.eventId) .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback)) .canVote(pollViewState.canVote) - .totalVotesText(pollViewState.totalVotes) + .votesStatus(pollViewState.votesStatus) .optionViewStates(pollViewState.optionViewStates) .edited(informationData.hasBeenEdited) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 8da0f2d279..13f63e86c4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -65,7 +65,7 @@ class PollItemViewStateFactory @Inject constructor( private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollViewState { return PollViewState( question = question, - totalVotes = stringProvider.getString(R.string.poll_no_votes_cast), + votesStatus = stringProvider.getString(R.string.poll_no_votes_cast), canVote = false, optionViewStates = pollCreationInfo?.answers?.map { answer -> PollOptionViewState.PollSending( @@ -85,7 +85,7 @@ class PollItemViewStateFactory @Inject constructor( ): PollViewState { return PollViewState( question = question, - totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), + votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), canVote = false, optionViewStates = pollCreationInfo?.answers?.map { answer -> val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") @@ -107,7 +107,7 @@ class PollItemViewStateFactory @Inject constructor( ): PollViewState { return PollViewState( question = question, - totalVotes = "", + votesStatus = stringProvider.getString(R.string.poll_undisclosed_not_ended), canVote = true, optionViewStates = pollCreationInfo?.answers?.map { answer -> val isMyVote = pollResponseSummary?.myVote == answer.id @@ -128,7 +128,7 @@ class PollItemViewStateFactory @Inject constructor( ): PollViewState { return PollViewState( question = question, - totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), + votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), canVote = true, optionViewStates = pollCreationInfo?.answers?.map { answer -> val isMyVote = pollResponseSummary?.myVote == answer.id @@ -152,7 +152,7 @@ class PollItemViewStateFactory @Inject constructor( } return PollViewState( question = question, - totalVotes = totalVotesText, + votesStatus = totalVotesText, canVote = true, optionViewStates = pollCreationInfo?.answers?.map { answer -> PollOptionViewState.PollReady( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index f52a770d4d..ad8d01b82e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @@ -177,6 +178,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen return true } + // Hide fake events for local rooms + if (RoomLocalEcho.isLocalEchoId(roomId) && + root.getClearType() == EventType.STATE_ROOM_MEMBER || + root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY) { + return true + } + // Allow only the the threads within the rootThreadEventId along with the root event if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) { return if (root.getRootThreadEventId() == rootThreadEventId) { 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 1e04b69e52..ffc1bec7dd 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 @@ -24,6 +24,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.appcompat.view.ContextThemeWrapper import androidx.core.content.ContextCompat.getDrawable import androidx.core.view.isVisible @@ -55,7 +56,7 @@ private const val MAX_REACTIONS_TO_SHOW = 8 * Manages associated click listeners and send status. * Should not be used as this, use a subclass. */ -abstract class AbsBaseMessageItem : BaseEventItem() { +abstract class AbsBaseMessageItem(@LayoutRes layoutId: Int) : BaseEventItem(layoutId) { abstract val baseAttributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 587ba739a1..5f4a65bfd8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -24,6 +24,7 @@ import android.widget.ProgressBar import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -50,7 +51,9 @@ import kotlin.math.ceil * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state. * Adds associated click listeners (on avatar, displayname). */ -abstract class AbsMessageItem : AbsBaseMessageItem() { +abstract class AbsMessageItem( + @LayoutRes layoutId: Int = R.layout.item_timeline_event_base +) : AbsBaseMessageItem(layoutId) { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt index 6b62b56302..b1acc923ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt @@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute @@ -36,7 +37,9 @@ import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvid import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners -abstract class AbsMessageLocationItem : AbsMessageItem() { +abstract class AbsMessageLocationItem( + @LayoutRes layoutId: Int = R.layout.item_timeline_event_base +) : AbsMessageItem(layoutId) { @EpoxyAttribute var locationUrl: String? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 0b2f2c6992..e4af9ae9d4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -22,6 +22,7 @@ import android.widget.FrameLayout import android.widget.RelativeLayout import androidx.annotation.CallSuper import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.R @@ -34,7 +35,7 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay /** * Children must override getViewType(). */ -abstract class BaseEventItem : VectorEpoxyModel(), ItemWithEvents, BubbleDependentView { +abstract class BaseEventItem(@LayoutRes layoutId: Int) : VectorEpoxyModel(layoutId), ItemWithEvents, BubbleDependentView { // To use for instance when opening a permalink with an eventId @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt index 877ad0e7c2..0fd88936b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.TextView import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import im.vector.app.R import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -26,7 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayo import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout import org.matrix.android.sdk.api.util.MatrixItem -abstract class BasedMergedItem : BaseEventItem() { +abstract class BasedMergedItem(@LayoutRes layoutId: Int) : BaseEventItem(layoutId) { abstract val attributes: Attributes @@ -59,6 +60,7 @@ abstract class BasedMergedItem : BaseEventItem() } data class Data( + val roomId: String?, val localId: Long, val eventId: String, val userId: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BlankItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BlankItem.kt index cf3e4a8328..bcb95f0414 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BlankItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BlankItem.kt @@ -20,7 +20,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_timeline_event_blank_stub) -abstract class BlankItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class BlankItem : VectorEpoxyModel(R.layout.item_timeline_event_blank_stub) { class BlankHolder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt index 228e0181d2..58d267fb0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -39,8 +39,8 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) -abstract class CallTileTimelineItem : AbsBaseMessageItem() { +@EpoxyModelClass +abstract class CallTileTimelineItem : AbsBaseMessageItem(R.layout.item_timeline_event_base_state) { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DaySeparatorItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DaySeparatorItem.kt index 7aebee73a6..f993b3ea08 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DaySeparatorItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DaySeparatorItem.kt @@ -21,12 +21,12 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_timeline_event_day_separator) -abstract class DaySeparatorItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class DaySeparatorItem : VectorEpoxyModel(R.layout.item_timeline_event_day_separator) { @EpoxyAttribute lateinit var formattedDay: String diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt index e6dfd18641..f2b27c245d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt @@ -26,8 +26,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) -abstract class DefaultItem : BaseEventItem() { +@EpoxyModelClass +abstract class DefaultItem : BaseEventItem(R.layout.item_timeline_event_base_noinfo) { @EpoxyAttribute lateinit var attributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index a73f91010c..e1022fd5ed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -25,9 +25,9 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -39,12 +39,14 @@ import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.tools.linkify +import im.vector.app.features.themes.ThemeUtils import me.gujun.android.span.span 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.util.toMatrixItem -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) -abstract class MergedRoomCreationItem : BasedMergedItem() { +@EpoxyModelClass +abstract class MergedRoomCreationItem : BasedMergedItem(R.layout.item_timeline_event_base_noinfo) { @EpoxyAttribute override lateinit var attributes: Attributes @@ -52,126 +54,123 @@ abstract class MergedRoomCreationItem : BasedMergedItem { this.marginEnd = leftGuideline } if (attributes.isEncryptionAlgorithmSecure) { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) - holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) { - holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description) - } else { - holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) - } - holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), - null, null, null - ) + renderE2ESecureTile(holder) } else { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) - holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), - null, null, null - ) + renderE2EUnsecureTile(holder) } } else { holder.encryptionTile.isVisible = false } } + private fun renderE2ESecureTile(holder: Holder) { + val resources = holder.expandView.resources + val description = when { + isDirectRoom -> { + if (attributes.isLocalRoom) { + resources.getString(R.string.direct_room_encryption_enabled_tile_description_future) + } else { + resources.getString(R.string.direct_room_encryption_enabled_tile_description) + } + } + else -> { + resources.getString(R.string.encryption_enabled_tile_description) + } + } + + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), + null, null, null + ) + holder.e2eTitleDescriptionView.text = description + holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER + } + + private fun renderE2EUnsecureTile(holder: Holder) { + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) + holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), + null, null, null + ) + } + private fun bindCreationSummaryTile(holder: Holder) { - val roomSummary = attributes.roomSummary val roomDisplayName = roomSummary?.displayName - holder.roomNameText.setTextOrHide(roomDisplayName) - val isDirect = roomSummary?.isDirect == true val membersCount = roomSummary?.otherMemberIds?.size ?: 0 - if (isDirect) { - holder.roomDescriptionText.text = holder.view.resources.getString( - R.string.this_is_the_beginning_of_dm, - roomSummary?.displayName ?: "" - ) - } else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) { - holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name) - } else { - holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName) - } - - val topic = roomSummary?.topic - if (topic.isNullOrBlank()) { - // do not show hint for DMs or group DMs - val canSetTopic = attributes.canChangeTopic && !isDirect - if (canSetTopic) { - val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text) - val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink)) - holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() { - override fun onClick(widget: View) { - attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic) - } - })) - } - } else { - holder.roomTopicText.setTextOrHide( - span { - span(holder.view.resources.getString(R.string.topic_prefix)) { - textStyle = "bold" - } - +topic.linkify(attributes.callback) - } - ) - } - holder.roomTopicText.movementMethod = movementMethod + holder.roomNameText.setTextOrHide(roomDisplayName) + renderRoomDescription(holder) + renderRoomTopic(holder) val roomItem = roomSummary?.toMatrixItem() val shouldSetAvatar = attributes.canChangeAvatar && - (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) && + (roomSummary?.isDirect == false || (isDirectRoom && membersCount >= 2)) && roomItem?.avatarUrl.isNullOrBlank() holder.roomAvatarImageView.isVisible = roomItem != null @@ -194,7 +193,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem { + if (attributes.isLocalRoom) { + resources.getString(R.string.send_your_first_msg_to_invite, roomSummary?.displayName.orEmpty()) + } else { + resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName.orEmpty()) + } + } + roomDisplayName.isNullOrBlank() || roomSummary?.name.isNullOrBlank() -> { + holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name) + } + else -> { + holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName) + } + } + holder.roomDescriptionText.text = description + if (isDirectRoom && attributes.isLocalRoom) { + TextViewCompat.setTextAppearance(holder.roomDescriptionText, R.style.TextAppearance_Vector_Subtitle) + holder.roomDescriptionText.setTextColor(ThemeUtils.getColor(holder.roomDescriptionText.context, R.attr.vctr_content_primary)) + } + } + + private fun renderRoomTopic(holder: Holder) { + val topic = roomSummary?.topic + if (topic.isNullOrBlank()) { + // do not show hint for DMs or group DMs + val canSetTopic = attributes.canChangeTopic && !isDirectRoom + if (canSetTopic) { + val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text) + val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink)) + holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() { + override fun onClick(widget: View) { + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic) + } + })) + } + } else { + holder.roomTopicText.setTextOrHide( + span { + span(holder.view.resources.getString(R.string.topic_prefix)) { + textStyle = "bold" + } + +topic.linkify(attributes.callback) + } + ) + } + holder.roomTopicText.movementMethod = movementMethod + } + class Holder : BasedMergedItem.Holder(STUB_ID) { + val mergedView by bind(R.id.mergedSumContainer) val summaryView by bind(R.id.itemNoticeTextView) val avatarView by bind(R.id.itemNoticeAvatarView) val encryptionTile by bind(R.id.creationEncryptionTile) @@ -238,5 +290,8 @@ abstract class MergedRoomCreationItem : BasedMergedItem() { +@EpoxyModelClass +abstract class MergedSimilarEventsItem : BasedMergedItem(R.layout.item_timeline_event_base_noinfo) { override fun getViewStubId() = STUB_ID 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 e05e9b3fba..071d801df9 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 @@ -37,7 +37,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageAudioItem : AbsMessageItem() { @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index ca46c643d2..f51eaa8fa7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -37,7 +37,7 @@ import im.vector.app.features.themes.guessTextWidth import kotlin.math.ceil import kotlin.math.max -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageFileItem : AbsMessageItem() { @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 1567a83128..6786f8948e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -44,7 +44,7 @@ import kotlin.math.round import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.util.MimeTypes -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageImageVideoItem : AbsMessageItem() { @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt index bb85316bf1..bc6e96b0ee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt @@ -21,7 +21,7 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageLiveLocationInactiveItem : AbsMessageItem(), LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt index f875509bc8..f41c3a356f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt @@ -30,7 +30,7 @@ import im.vector.app.features.location.live.LocationLiveMessageBannerView import im.vector.app.features.location.live.LocationLiveMessageBannerViewState import org.threeten.bp.LocalDateTime -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageLiveLocationItem : AbsMessageLocationItem() { @EpoxyAttribute diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt index 001774b579..a847c24f57 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt @@ -21,7 +21,7 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageLiveLocationStartItem : AbsMessageItem(), LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 37f728407b..b5f513fbe9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageLocationItem : AbsMessageLocationItem() { override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index c984609e43..af420d6b34 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -41,7 +41,7 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import io.noties.markwon.MarkwonPlugin import org.matrix.android.sdk.api.extensions.orFalse -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute 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 c227937f4d..08a7267000 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 @@ -39,7 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.voice.AudioWaveformView -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class MessageVoiceItem : AbsMessageItem() { interface WaveformTouchListener { 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 66376afd00..fa73fe19b1 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 @@ -32,8 +32,8 @@ import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessage import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) -abstract class NoticeItem : BaseEventItem() { +@EpoxyModelClass +abstract class NoticeItem : BaseEventItem(R.layout.item_timeline_event_base_noinfo) { @EpoxyAttribute lateinit var attributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 273dd0299a..5dd721b007 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -26,7 +26,7 @@ import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class PollItem : AbsMessageItem() { @EpoxyAttribute @@ -42,7 +42,7 @@ abstract class PollItem : AbsMessageItem() { var canVote: Boolean = false @EpoxyAttribute - var totalVotesText: String? = null + var votesStatus: String? = null @EpoxyAttribute var edited: Boolean = false @@ -58,7 +58,7 @@ abstract class PollItem : AbsMessageItem() { renderSendState(holder.view, holder.questionTextView) holder.questionTextView.text = pollQuestion?.charSequence - holder.totalVotesTextView.text = totalVotesText + holder.votesStatusTextView.text = votesStatus while (holder.optionsContainer.childCount < optionViewStates.size) { holder.optionsContainer.addView(PollOptionView(holder.view.context)) @@ -88,7 +88,7 @@ abstract class PollItem : AbsMessageItem() { class Holder : AbsMessageItem.Holder(STUB_ID) { val questionTextView by bind(R.id.questionTextView) val optionsContainer by bind(R.id.optionsContainer) - val totalVotesTextView by bind(R.id.optionsTotalVotesTextView) + val votesStatusTextView by bind(R.id.optionsVotesStatusTextView) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt index b21b1a152d..20aa6e3af2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt @@ -82,6 +82,7 @@ class PollOptionView @JvmOverloads constructor( private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) { views.optionCheckImageView.isVisible = true views.optionWinnerImageView.isVisible = false + hideVotes() renderVoteSelection(state.isSelected) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt index eca8caec75..6b18947c6c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt @@ -22,10 +22,10 @@ import android.widget.FrameLayout import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale @@ -36,8 +36,8 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView import im.vector.app.features.home.room.detail.timeline.view.setFlatRtl -@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts) -abstract class ReadReceiptsItem : EpoxyModelWithHolder(), ItemWithEvents, BubbleDependentView { +@EpoxyModelClass +abstract class ReadReceiptsItem : VectorEpoxyModel(R.layout.item_timeline_event_read_receipts), ItemWithEvents, BubbleDependentView { @EpoxyAttribute lateinit var eventId: String @EpoxyAttribute lateinit var readReceipts: List diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt index 204bab2254..057a9bad0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +@EpoxyModelClass abstract class RedactedMessageItem : AbsMessageItem() { override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt index a6d2bcc66d..394079ed5f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -25,8 +25,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import me.saket.bettermovementmethod.BetterLinkMovementMethod -@EpoxyModelClass(layout = R.layout.item_timeline_event_create) -abstract class RoomCreateItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomCreateItem : VectorEpoxyModel(R.layout.item_timeline_event_create) { @EpoxyAttribute lateinit var text: EpoxyCharSequence diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index e4c68a7a5b..c4dd4bedeb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -31,8 +31,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) -abstract class StatusTileTimelineItem : AbsBaseMessageItem() { +@EpoxyModelClass +abstract class StatusTileTimelineItem : AbsBaseMessageItem(R.layout.item_timeline_event_base_state) { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt index 106f178216..700a8d6e2c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt @@ -21,8 +21,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_timeline_read_marker) -abstract class TimelineReadMarkerItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class TimelineReadMarkerItem : VectorEpoxyModel(R.layout.item_timeline_read_marker) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt index 395b5fa308..1abed2031d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -39,8 +39,8 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationState -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) -abstract class VerificationRequestItem : AbsBaseMessageItem() { +@EpoxyModelClass +abstract class VerificationRequestItem : AbsBaseMessageItem(R.layout.item_timeline_event_base_state) { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt index a3a84910f5..5a73a20aa3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt @@ -32,8 +32,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) -abstract class WidgetTileTimelineItem : AbsBaseMessageItem() { +@EpoxyModelClass +abstract class WidgetTileTimelineItem : AbsBaseMessageItem(R.layout.item_timeline_event_base_state) { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt index 9984cda38a..d56889c2a3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt @@ -21,11 +21,11 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.glide.renderReactionImage import im.vector.app.core.utils.DimensionConverter @@ -34,8 +34,8 @@ import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * Item displaying an emoji reaction (single line with emoji, author, time). */ -@EpoxyModelClass(layout = R.layout.item_simple_reaction_info) -abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class ReactionInfoSimpleItem : VectorEpoxyModel(R.layout.item_simple_reaction_info) { @EpoxyAttribute lateinit var reactionKey: EpoxyCharSequence diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt index 4ffc81fc1e..210ce9cfca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt @@ -22,17 +22,17 @@ import androidx.annotation.DrawableRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.widgets.model.Widget import java.net.URL -@EpoxyModelClass(layout = R.layout.item_room_widget) -abstract class RoomWidgetItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class RoomWidgetItem : VectorEpoxyModel(R.layout.item_room_widget) { @EpoxyAttribute lateinit var widget: Widget @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var widgetClicked: ClickListener? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt index 78c4e81fe4..a89e7dc62f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.time.DefaultClock import im.vector.app.features.settings.VectorPreferences -@EpoxyModelClass(layout = R.layout.item_room_filter_footer) -abstract class FilteredRoomFooterItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class FilteredRoomFooterItem : VectorEpoxyModel(R.layout.item_room_filter_footer) { @EpoxyAttribute var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt index 59e0108981..2f0383c45c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.features.themes.ThemeUtils -@EpoxyModelClass(layout = R.layout.item_room_category_sc) -abstract class RoomCategoryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomCategoryItem : VectorEpoxyModel(R.layout.item_room_category_sc) { @EpoxyAttribute lateinit var title: String @EpoxyAttribute var itemCount: Int = 0 diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt index 28cc9a9bd0..581361addd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt @@ -34,8 +34,8 @@ import im.vector.app.features.invite.InviteButtonStateBinder import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_room_invitation) -abstract class RoomInvitationItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomInvitationItem : VectorEpoxyModel(R.layout.item_room_invitation) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem 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 818219164d..8b6cc479d1 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 @@ -187,6 +187,11 @@ class RoomListFragment @Inject constructor( } updateDebugView() + + roomListViewModel.onEach(RoomListViewState::localRoomIds) { + // Local rooms should not exist anymore when the room list is shown + roomListViewModel.deleteLocalRooms(it) + } } override fun onPause() { 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 0c53215d00..89baff07c5 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 @@ -51,7 +51,10 @@ 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 import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow @@ -109,6 +112,7 @@ class RoomListViewModel @AssistedInject constructor( init { observeMembershipChanges() + observeLocalRooms() appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() @@ -136,6 +140,23 @@ 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 = if (initialState.explicitSpaceId != SPACE_ID_FOLLOW_APP || @@ -190,6 +211,14 @@ 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 { 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 4f8f79b49e..67a5c24e20 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,6 +31,7 @@ data class RoomListViewState( val asyncSuggestedRooms: Async> = Uninitialized, val currentUserName: String? = null, val currentRoomGrouping: Async = Uninitialized, + val localRoomIds: Set = emptySet(), // In comparison to currentRoomGrouping, the explicit space id fixes a filter method that should not change afterwards val explicitSpaceId: String? = null ) : MavericksState { 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 5667673939..6db3da295f 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 @@ -43,8 +43,8 @@ import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_room) -abstract class RoomSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomSummaryItem : VectorEpoxyModel(R.layout.item_room) { @EpoxyAttribute lateinit var typingMessage: String diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt index 218cf0c812..597e8a372f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt @@ -40,8 +40,8 @@ import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_room_centered) -abstract class RoomSummaryItemCentered : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomSummaryItemCentered : VectorEpoxyModel(R.layout.item_room_centered) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer 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 96d8f6418b..d4683f78a5 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 @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_room_placeholder) -abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel(R.layout.item_room_placeholder) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt index a2cb905f1b..964a39fb0f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt @@ -40,8 +40,8 @@ import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_explore_space_child) -abstract class SpaceChildInfoItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceChildInfoItem : VectorEpoxyModel(R.layout.item_explore_space_child) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt index 1ae017c98c..6899b59f38 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_space_directory_filter_no_results) -abstract class SpaceDirectoryFilterNoResults : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceDirectoryFilterNoResults : VectorEpoxyModel(R.layout.item_space_directory_filter_no_results) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt index 3aa78b4349..af086c9e9a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt @@ -37,8 +37,8 @@ import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_thread) -abstract class ThreadListItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ThreadListItem : VectorEpoxyModel(R.layout.item_thread) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 13bf056301..aaa9846c39 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.threads.list.views import android.os.Bundle import android.view.LayoutInflater import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -31,6 +30,7 @@ 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.VectorMenuProvider import im.vector.app.databinding.FragmentThreadListBinding import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.AvatarRenderer @@ -54,7 +54,8 @@ class ThreadListFragment @Inject constructor( private val threadListController: ThreadListController, val threadListViewModelFactory: ThreadListViewModel.Factory ) : VectorBaseFragment(), - ThreadListController.Listener { + ThreadListController.Listener, + VectorMenuProvider { private val threadListViewModel: ThreadListViewModel by fragmentViewModel() @@ -71,27 +72,26 @@ class ThreadListFragment @Inject constructor( analyticsScreenName = MobileScreen.ScreenName.ThreadList } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - + override fun handlePostCreateMenu(menu: Menu) { + // We use a custom layout for this menu item, so we need to set a ClickListener menu.findItem(R.id.menu_thread_list_filter)?.let { menuItem -> - menuItem.actionView.setOnClickListener { - onOptionsItemSelected(menuItem) + menuItem.actionView.debouncedClicks { + handleMenuItemSelected(menuItem) } } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_thread_list_filter -> { ThreadListBottomSheet().show(childFragmentManager, "Filtering") true } - else -> super.onOptionsItemSelected(item) + else -> false } } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { withState(threadListViewModel) { state -> val filterIcon = menu.findItem(R.id.menu_thread_list_filter).actionView val filterBadge = filterIcon.findViewById(R.id.threadListFilterBadge) 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 c7a1dd3270..70687dfe8b 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 @@ -14,6 +14,15 @@ * limitations under the License. */ +/* + * This file renders the formatted_body of an event to a formatted Android Spannable. + * The core of this work is done with Markwon, a general-purpose Markdown+HTML formatter. + * Since formatted_body is HTML only, Markwon is configured to only handle HTML, not Markdown. + * The EventHtmlRenderer class is next used in the method buildFormattedTextItem + * in the file MessageItemFactory.kt. + * Effectively, this is used in the chat messages view and the room list message previews. + */ + package im.vector.app.features.html import android.content.Context @@ -116,21 +125,23 @@ class EventHtmlRenderer @Inject constructor( )) .apply { if (vectorPreferences.latexMathsIsEnabled()) { - usePlugin(object : AbstractMarkwonPlugin() { // Markwon expects maths to be in a specific format: https://noties.io/Markwon/docs/v4/ext-latex - override fun processMarkdown(markdown: String): String { - return markdown - .replace(Regex(""".*?""")) { matchResult -> - "$$" + matchResult.groupValues[1] + "$$" - } - .replace(Regex(""".*?""")) { matchResult -> - "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" - } - } - }) - .usePlugin(JLatexMathPlugin.create(44F) { builder -> - builder.inlinesEnabled(true) - builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8)) - }) + // If latex maths is enabled in app preferences, refomat it so Markwon recognises it + // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex + usePlugin(object : AbstractMarkwonPlugin() { + override fun processMarkdown(markdown: String): String { + return markdown + .replace(Regex(""".*?""")) { matchResult -> + "$$" + matchResult.groupValues[1] + "$$" + } + .replace(Regex(""".*?""")) { matchResult -> + "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" + } + } + }) + .usePlugin(JLatexMathPlugin.create(44F) { builder -> + builder.inlinesEnabled(true) + builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8)) + }) } } .usePlugin( @@ -146,6 +157,9 @@ class EventHtmlRenderer @Inject constructor( ) .usePlugin(object : AbstractMarkwonPlugin() { override fun configureParser(builder: Parser.Builder) { + /* Configuring the Markwon block formatting processor. + * Default settings are all Markdown blocks. Turn those off. + */ builder.enabledBlockTypes(kotlin.collections.emptySet()) } }) @@ -221,6 +235,7 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C override fun configureHtml(plugin: HtmlPlugin) { plugin + .addHandler(ListHandlerWithInitialStart()) .addHandler(FontTagHandler()) .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) diff --git a/vector/src/main/java/im/vector/app/features/html/ListHandlerWithInitialStart.java b/vector/src/main/java/im/vector/app/features/html/ListHandlerWithInitialStart.java new file mode 100644 index 0000000000..c7ba881da0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/ListHandlerWithInitialStart.java @@ -0,0 +1,111 @@ +/* + * 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.html; + +import androidx.annotation.NonNull; + +import org.commonmark.node.ListItem; + +import java.util.Arrays; +import java.util.Collection; + +import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.RenderProps; +import io.noties.markwon.SpanFactory; +import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.core.CoreProps; +import io.noties.markwon.html.HtmlTag; +import io.noties.markwon.html.MarkwonHtmlRenderer; +import io.noties.markwon.html.TagHandler; + +/** + * Copied from https://github.com/noties/Markwon/blob/master/markwon-html/src/main/java/io/noties/markwon/html/tag/ListHandler.java#L44 + * With a modification on the starting list position + */ +public class ListHandlerWithInitialStart extends TagHandler { + + private static final String START_KEY = "start"; + + @Override + public void handle( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag) { + + if (!tag.isBlock()) { + return; + } + + final HtmlTag.Block block = tag.getAsBlock(); + final boolean ol = "ol".equals(block.name()); + final boolean ul = "ul".equals(block.name()); + + if (!ol && !ul) { + return; + } + + final MarkwonConfiguration configuration = visitor.configuration(); + final RenderProps renderProps = visitor.renderProps(); + final SpanFactory spanFactory = configuration.spansFactory().get(ListItem.class); + + // Modified line + int number = Integer.parseInt(block.attributes().containsKey(START_KEY) ? block.attributes().get(START_KEY) : "1"); + + final int bulletLevel = currentBulletListLevel(block); + + for (HtmlTag.Block child : block.children()) { + + visitChildren(visitor, renderer, child); + + if (spanFactory != null && "li".equals(child.name())) { + + // insert list item here + if (ol) { + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.ORDERED); + CoreProps.ORDERED_LIST_ITEM_NUMBER.set(renderProps, number++); + } else { + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.BULLET); + CoreProps.BULLET_LIST_ITEM_LEVEL.set(renderProps, bulletLevel); + } + + SpannableBuilder.setSpans( + visitor.builder(), + spanFactory.getSpans(configuration, renderProps), + child.start(), + child.end()); + } + } + } + + @NonNull + @Override + public Collection supportedTags() { + return Arrays.asList("ol", "ul"); + } + + private static int currentBulletListLevel(@NonNull HtmlTag.Block block) { + int level = 0; + while ((block = block.parent()) != null) { + if ("ul".equals(block.name()) + || "ol".equals(block.name())) { + level += 1; + } + } + return level; + } +} diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 304ec72ce2..883c879e90 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -71,7 +71,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { when (sharedAction) { UserListSharedAction.Close -> finish() UserListSharedAction.GoBack -> onBackPressed() - is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) + is UserListSharedAction.OnMenuItemSubmitClick -> handleOnMenuItemSubmitClick(sharedAction) UserListSharedAction.OpenPhoneBook -> openPhoneBook() // not exhaustive because it's a sharedAction else -> Unit @@ -85,6 +85,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { UserListFragmentArgs( title = getString(R.string.invite_users_to_room_title), menuResId = R.menu.vector_invite_users_to_room, + submitMenuItemId = R.id.action_invite_users_to_room_invite, excludedUserIds = viewModel.getUserIdsOfRoomMembers(), showInviteActions = false ) @@ -94,10 +95,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() { viewModel.observeViewEvents { renderInviteEvents(it) } } - private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { - if (action.itemId == R.id.action_invite_users_to_room_invite) { - viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections)) - } + private fun handleOnMenuItemSubmitClick(action: UserListSharedAction.OnMenuItemSubmitClick) { + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections)) } private fun openPhoneBook() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt index 7fce09cad7..131119a7aa 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -26,6 +26,7 @@ import com.airbnb.mvrx.args import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.openLocation import im.vector.app.databinding.FragmentLocationPreviewBinding import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider @@ -38,7 +39,8 @@ import javax.inject.Inject class LocationPreviewFragment @Inject constructor( private val urlMapProvider: UrlMapProvider, private val locationPinProvider: LocationPinProvider -) : VectorBaseFragment() { +) : VectorBaseFragment(), + VectorMenuProvider { private val args: LocationSharingArgs by args() @@ -99,14 +101,14 @@ class LocationPreviewFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_location_preview - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.share_external -> { onShareLocationExternal() - return true + true } + else -> false } - return super.onOptionsItemSelected(item) } private fun onShareLocationExternal() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt rename to vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt index 8073aaaa35..69ffc0e89e 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt @@ -21,8 +21,9 @@ import android.os.Binder import android.os.IBinder import android.os.Parcelable import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.services.VectorService +import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.session.coroutineScope @@ -41,7 +42,7 @@ import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class LocationSharingService : VectorService(), LocationTracker.Callback { +class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Callback { @Parcelize data class RoomArgs( @@ -79,7 +80,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { launchWithActiveSession { session -> val job = locationTracker.locations - .onEach(this@LocationSharingService::onLocationUpdate) + .onEach(this@LocationSharingAndroidService::onLocationUpdate) .launchIn(session.coroutineScope) jobs.add(job) } @@ -112,7 +113,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { val updateLiveResult = session .getRoom(roomArgs.roomId) ?.locationSharingService() - ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis) + ?.startLiveLocationShare( + timeoutMillis = roomArgs.durationMillis, + description = getString(R.string.sent_live_location) + ) updateLiveResult ?.let { result -> @@ -221,7 +225,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } inner class LocalBinder : Binder() { - fun getService(): LocationSharingService = this@LocationSharingService + fun getService(): LocationSharingAndroidService = this@LocationSharingAndroidService } interface Callback { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index f881e694b6..be5f0aed6f 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -179,10 +179,10 @@ class LocationSharingFragment @Inject constructor( } private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) { - val args = LocationSharingService.RoomArgs(event.sessionId, event.roomId, event.durationMillis) + val args = LocationSharingAndroidService.RoomArgs(event.sessionId, event.roomId, event.durationMillis) - Intent(requireContext(), LocationSharingService::class.java) - .putExtra(LocationSharingService.EXTRA_ROOM_ARGS, args) + Intent(requireContext(), LocationSharingAndroidService::class.java) + .putExtra(LocationSharingAndroidService.EXTRA_ROOM_ARGS, args) .also { ContextCompat.startForegroundService(requireContext(), it) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index db79564462..495b780188 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -27,7 +27,8 @@ import javax.inject.Singleton @Singleton class LocationSharingServiceConnection @Inject constructor( private val context: Context -) : ServiceConnection, LocationSharingService.Callback { +) : ServiceConnection, + LocationSharingAndroidService.Callback { interface Callback { fun onLocationServiceRunning() @@ -37,7 +38,7 @@ class LocationSharingServiceConnection @Inject constructor( private val callbacks = mutableSetOf() private var isBound = false - private var locationSharingService: LocationSharingService? = null + private var locationSharingAndroidService: LocationSharingAndroidService? = null fun bind(callback: Callback) { addCallback(callback) @@ -45,7 +46,7 @@ class LocationSharingServiceConnection @Inject constructor( if (isBound) { callback.onLocationServiceRunning() } else { - Intent(context, LocationSharingService::class.java).also { intent -> + Intent(context, LocationSharingAndroidService::class.java).also { intent -> context.bindService(intent, this, 0) } } @@ -56,7 +57,7 @@ class LocationSharingServiceConnection @Inject constructor( } override fun onServiceConnected(className: ComponentName, binder: IBinder) { - locationSharingService = (binder as LocationSharingService.LocalBinder).getService().also { + locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { it.callback = this } isBound = true @@ -65,8 +66,8 @@ class LocationSharingServiceConnection @Inject constructor( override fun onServiceDisconnected(className: ComponentName) { isBound = false - locationSharingService?.callback = null - locationSharingService = null + locationSharingAndroidService?.callback = null + locationSharingAndroidService = null onCallbackActionNoArg(Callback::onLocationServiceStopped) } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt index c07104e72c..e8a6b22b7e 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt @@ -26,8 +26,6 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.toTimestamp import im.vector.app.core.time.Clock import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.location.live.map.bottomsheet.LiveLocationUserItem -import im.vector.app.features.location.live.map.bottomsheet.liveLocationUserItem import javax.inject.Inject class LiveLocationBottomSheetController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt index 336f688434..0a4742fafc 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.location.live.map.bottomsheet +package im.vector.app.features.location.live.map import android.widget.Button import android.widget.ImageView @@ -34,8 +34,8 @@ import im.vector.lib.core.utils.timer.CountUpTimer import org.matrix.android.sdk.api.util.MatrixItem import org.threeten.bp.Duration -@EpoxyModelClass(layout = R.layout.item_live_location_users_bottom_sheet) -abstract class LiveLocationUserItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LiveLocationUserItem : VectorEpoxyModel(R.layout.item_live_location_users_bottom_sheet) { interface Callback { fun onUserSelected(userId: String) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 722133f585..4cbebd67a3 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -37,6 +37,7 @@ import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.HomeActivity @@ -231,9 +232,9 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA } private fun inferAuthDescription(loginViewState: LoginViewState) = when (loginViewState.signMode) { - SignMode.Unknown -> null - SignMode.SignUp -> AuthenticationDescription.Register(type = AuthenticationDescription.AuthenticationType.Other) - SignMode.SignIn -> AuthenticationDescription.Login + SignMode.Unknown -> null + SignMode.SignUp -> AuthenticationDescription.Register(type = AuthenticationDescription.AuthenticationType.Other) + SignMode.SignIn -> AuthenticationDescription.Login SignMode.SignInWithMatrixId -> AuthenticationDescription.Login } @@ -272,8 +273,8 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { - LoginMode.Unknown, - is LoginMode.Sso -> error("Developer error") + LoginMode.Unknown -> error("Developer error") + is LoginMode.Sso -> launchSsoFlow() is LoginMode.SsoAndPassword, LoginMode.Password -> addFragmentToBackstack( views.loginFragmentContainer, @@ -293,6 +294,16 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA } } + private fun launchSsoFlow() = withState(loginViewModel) { state -> + loginViewModel.getSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null, + )?.let { ssoUrl -> + openUrlInChromeCustomTab(this, null, ssoUrl) + } + } + /** * Handle the SSO redirection here. */ diff --git a/vector/src/main/java/im/vector/app/features/login/terms/PolicyItem.kt b/vector/src/main/java/im/vector/app/features/login/terms/PolicyItem.kt index ff35f912cf..f35de49ecc 100644 --- a/vector/src/main/java/im/vector/app/features/login/terms/PolicyItem.kt +++ b/vector/src/main/java/im/vector/app/features/login/terms/PolicyItem.kt @@ -21,15 +21,15 @@ import android.widget.CompoundButton import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setHorizontalPadding -@EpoxyModelClass(layout = R.layout.item_policy) -abstract class PolicyItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class PolicyItem : VectorEpoxyModel(R.layout.item_policy) { @EpoxyAttribute var checked: Boolean = false 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 6bdcc9724e..95b3d2e5bd 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 @@ -49,7 +49,7 @@ import im.vector.app.R import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.resources.StringProvider -import im.vector.app.core.services.CallService +import im.vector.app.core.services.CallAndroidService import im.vector.app.core.time.Clock import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity @@ -511,7 +511,7 @@ class NotificationUtils @Inject constructor( /** * Build notification for the CallService, when a call is missed. */ - fun buildCallMissedNotification(callInformation: CallService.CallInformation): Notification { + fun buildCallMissedNotification(callInformation: CallAndroidService.CallInformation): Notification { val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId) .apply { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index b6a7550a58..96b0bc45d6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -47,7 +47,9 @@ sealed interface OnboardingAction : VectorViewModelAction { data class LoginWithToken(val loginToken: String) : OnboardingAction data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction - data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction + data class ResetPassword(val email: String, val newPassword: String?) : OnboardingAction + data class ConfirmNewPassword(val newPassword: String, val signOutAllDevices: Boolean) : OnboardingAction + object ResendResetPassword : OnboardingAction object ResetPasswordMailConfirmed : OnboardingAction data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index bf53a72cc3..ea6981a2b5 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -47,9 +47,11 @@ sealed class OnboardingViewEvents : VectorViewEvents { object OnHomeserverEdited : OnboardingViewEvents() data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() object OnForgetPasswordClicked : OnboardingViewEvents() - object OnResetPasswordSendThreePidDone : OnboardingViewEvents() - object OnResetPasswordMailConfirmationSuccess : OnboardingViewEvents() - object OnResetPasswordMailConfirmationSuccessDone : OnboardingViewEvents() + + data class OnResetPasswordEmailConfirmationSent(val email: String) : OnboardingViewEvents() + object OpenResetPasswordComplete : OnboardingViewEvents() + object OnResetPasswordBreakerConfirmed : OnboardingViewEvents() + object OnResetPasswordComplete : OnboardingViewEvents() data class OnSendEmailSuccess(val email: String) : OnboardingViewEvents() data class OnSendMsisdnSuccess(val msisdn: String) : OnboardingViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 38a72441e0..27c501176e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -149,6 +149,8 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) + OnboardingAction.ResendResetPassword -> handleResendResetPassword() + is OnboardingAction.ConfirmNewPassword -> handleResetPasswordConfirmed(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) is OnboardingAction.ResetAction -> handleResetAction(action) @@ -300,9 +302,12 @@ class OnboardingViewModel @AssistedInject constructor( authenticationDescription = awaitState().selectedAuthenticationState.description ?: AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Other) ) - RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) + RegistrationActionHandler.Result.StartRegistration -> { + overrideNextStage?.invoke() ?: _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration) + } RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback) is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationActionHandler.Result.SendMsisdnSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendMsisdnSuccess(it.msisdn.msisdn)) is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) RegistrationActionHandler.Result.MissingNextStage -> { _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found"))) @@ -439,25 +444,9 @@ class OnboardingViewModel @AssistedInject constructor( } private fun handleResetPassword(action: OnboardingAction.ResetPassword) { - val safeLoginWizard = loginWizard - setState { copy(isLoading = true) } - currentJob = viewModelScope.launch { - runCatching { safeLoginWizard.resetPassword(action.email) }.fold( - onSuccess = { - val state = awaitState() - setState { - copy( - isLoading = false, - resetState = createResetState(action, state.selectedHomeserver) - ) - } - _viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone) - }, - onFailure = { - setState { copy(isLoading = false) } - _viewEvents.post(OnboardingViewEvents.Failure(it)) - } - ) + startResetPasswordFlow(action.email) { + setState { copy(isLoading = false, resetState = createResetState(action, selectedHomeserver)) } + _viewEvents.post(OnboardingViewEvents.OnResetPasswordEmailConfirmationSent(action.email)) } } @@ -467,6 +456,41 @@ class OnboardingViewModel @AssistedInject constructor( supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported ) + private fun handleResendResetPassword() { + withState { state -> + val resetState = state.resetState + when (resetState.email) { + null -> _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No reset email has been set"))) + else -> { + startResetPasswordFlow(resetState.email) { + setState { copy(isLoading = false) } + } + } + } + } + } + + private fun startResetPasswordFlow(email: String, onSuccess: suspend () -> Unit) { + val safeLoginWizard = loginWizard + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + runCatching { safeLoginWizard.resetPassword(email) }.fold( + onSuccess = { onSuccess.invoke() }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + ) + } + } + + private fun handleResetPasswordConfirmed(action: OnboardingAction.ConfirmNewPassword) { + setState { copy(isLoading = true) } + currentJob = viewModelScope.launch { + confirmPasswordReset(action.newPassword, action.signOutAllDevices) + } + } + private fun handleResetPasswordMailConfirmed() { setState { copy(isLoading = true) } currentJob = viewModelScope.launch { @@ -476,27 +500,28 @@ class OnboardingViewModel @AssistedInject constructor( setState { copy(isLoading = false) } _viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("Developer error - No new password has been set"))) } - else -> { - runCatching { loginWizard.resetPasswordMailConfirmed(newPassword) }.fold( - onSuccess = { - setState { - copy( - isLoading = false, - resetState = ResetState() - ) - } - _viewEvents.post(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess) - }, - onFailure = { - setState { copy(isLoading = false) } - _viewEvents.post(OnboardingViewEvents.Failure(it)) - } - ) - } + else -> confirmPasswordReset(newPassword, logoutAllDevices = true) } } } + private suspend fun confirmPasswordReset(newPassword: String, logoutAllDevices: Boolean) { + runCatching { loginWizard.resetPasswordMailConfirmed(newPassword, logoutAllDevices = logoutAllDevices) }.fold( + onSuccess = { + setState { copy(isLoading = false, resetState = ResetState()) } + val nextEvent = when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete + else -> OnboardingViewEvents.OpenResetPasswordComplete + } + _viewEvents.post(nextEvent) + }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + ) + } + private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) { setState { copy(isLoading = true) } currentJob = viewModelScope.launch { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index 76b1492cc3..488a96beb9 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -26,6 +26,7 @@ import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesCom import kotlinx.coroutines.flow.first import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session import javax.inject.Inject @@ -47,7 +48,8 @@ class RegistrationActionHandler @Inject constructor( else -> when (result) { is RegistrationResult.Complete -> Result.RegistrationComplete(result.session) is RegistrationResult.NextStep -> processFlowResult(result, state) - is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email) + is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email.email) + is RegistrationResult.SendMsisdnSuccess -> Result.SendMsisdnSuccess(result.msisdn) is RegistrationResult.Error -> Result.Error(result.cause) } } @@ -95,6 +97,7 @@ class RegistrationActionHandler @Inject constructor( data class NextStage(val stage: Stage) : Result data class Error(val cause: Throwable) : Result data class SendEmailSuccess(val email: String) : Result + data class SendMsisdnSuccess(val msisdn: RegisterThreePid.Msisdn) : Result object MissingNextStage : Result object StartRegistration : Result object UnsupportedStage : Result diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt index e27aa9b2ab..8635c1e203 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt @@ -59,7 +59,8 @@ class RegistrationWizardActionDelegate @Inject constructor( onSuccess = { it.toRegistrationResult() }, onFailure = { when { - action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) + action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid) + action.threePid is RegisterThreePid.Msisdn && it.is401() -> RegistrationResult.SendMsisdnSuccess(action.threePid) else -> RegistrationResult.Error(it) } } @@ -95,7 +96,8 @@ sealed interface RegistrationResult { data class Error(val cause: Throwable) : RegistrationResult data class Complete(val session: Session) : RegistrationResult data class NextStep(val flowResult: FlowResult) : RegistrationResult - data class SendEmailSuccess(val email: String) : RegistrationResult + data class SendEmailSuccess(val email: RegisterThreePid.Email) : RegistrationResult + data class SendMsisdnSuccess(val msisdn: RegisterThreePid.Msisdn) : RegistrationResult } sealed interface RegisterAction { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index 205a604aab..d9086952da 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -61,6 +61,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor( views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.loginPasswordInput.setOnImeDoneListener { submit() } views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) } + views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) } } private fun setupSubmitButton() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt index b689f40837..bc44a7dbdb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt @@ -21,8 +21,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import androidx.lifecycle.lifecycleScope import im.vector.app.R +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.realignPercentagesToParent @@ -34,10 +34,7 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.isHomeserverUnavailable -import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFtueAuthFragment() { @@ -66,9 +63,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt } views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) } views.chooseServerSubmit.debouncedClicks { updateServerUrl() } - views.chooseServerInput.editText().textChanges() - .onEach { views.chooseServerInput.error = null } - .launchIn(viewLifecycleOwner.lifecycleScope) + views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner) } private fun updateServerUrl() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt index ea376709f5..1d85c75fa1 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -20,19 +20,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.autofillEmail +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content -import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.databinding.FragmentFtueEmailInputBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.RegisterAction -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.auth.registration.RegisterThreePid -import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment() { @@ -47,16 +44,11 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen } private fun setupViews() { - views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit) + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() }) views.emailEntryInput.setOnImeDoneListener { updateEmail() } + views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner) views.emailEntrySubmit.debouncedClicks { updateEmail() } - - views.emailEntryInput.editText().textChanges() - .onEach { - views.emailEntryInput.error = null - views.emailEntrySubmit.isEnabled = it.isEmail() - } - .launchIn(viewLifecycleOwner.lifecycleScope) + views.emailEntryInput.autofillEmail() } private fun updateEmail() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index 5e4c954300..edfbcd89b6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -36,7 +36,6 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.onboarding.OnboardingAction -import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.RegisterAction import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -226,12 +225,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } TextInputFormFragmentMode.SetMsisdn -> { - if (throwable.is401()) { - // This is normal use case, we go to the enter code screen - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendMsisdnSuccess(viewModel.currentThreePid ?: ""))) - } else { - views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) - } + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } TextInputFormFragmentMode.ConfirmMsisdn -> { when { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneConfirmationFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneConfirmationFragment.kt new file mode 100644 index 0000000000..39577efa19 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneConfirmationFragment.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +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 im.vector.app.R +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtuePhoneConfirmationBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.failure.Failure +import javax.inject.Inject + +@Parcelize +data class FtueAuthPhoneConfirmationFragmentArgument( + val msisdn: String +) : Parcelable + +class FtueAuthPhoneConfirmationFragment @Inject constructor() : AbstractFtueAuthFragment() { + + private val params: FtueAuthPhoneConfirmationFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtuePhoneConfirmationBinding { + return FragmentFtuePhoneConfirmationBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.phoneConfirmationHeaderSubtitle.text = getString(R.string.ftue_auth_phone_confirmation_subtitle, params.msisdn) + views.phoneConfirmationInput.associateContentStateWith(button = views.phoneConfirmationSubmit) + views.phoneConfirmationInput.setOnImeDoneListener { submitConfirmationCode() } + views.phoneConfirmationInput.clearErrorOnChange(viewLifecycleOwner) + views.phoneConfirmationSubmit.debouncedClicks { submitConfirmationCode() } + views.phoneConfirmationResend.debouncedClicks { viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid)) } + } + + private fun submitConfirmationCode() { + val code = views.phoneConfirmationInput.content() + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(code))) + } + + override fun onError(throwable: Throwable) { + views.phoneConfirmationInput.error = when (throwable) { + // The entered code is not correct + is Failure.SuccessError -> getString(R.string.login_validation_code_is_not_correct) + else -> errorFormatter.toHumanReadable(throwable) + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt new file mode 100644 index 0000000000..905af75639 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.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.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import im.vector.app.R +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.autofillPhoneNumber +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtuePhoneInputBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import reactivecircus.flowbinding.android.widget.textChanges +import javax.inject.Inject + +class FtueAuthPhoneEntryFragment @Inject constructor( + private val phoneNumberParser: PhoneNumberParser +) : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtuePhoneInputBinding { + return FragmentFtuePhoneInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.phoneEntryInput.associateContentStateWith(button = views.phoneEntrySubmit) + views.phoneEntryInput.setOnImeDoneListener { updatePhoneNumber() } + views.phoneEntrySubmit.debouncedClicks { updatePhoneNumber() } + + views.phoneEntryInput.editText().textChanges() + .onEach { + views.phoneEntryInput.error = null + views.phoneEntrySubmit.isEnabled = it.isNotBlank() + } + .launchIn(viewLifecycleOwner.lifecycleScope) + + views.phoneEntryInput.autofillPhoneNumber() + } + + private fun updatePhoneNumber() { + val number = views.phoneEntryInput.content() + + when (val result = phoneNumberParser.parseInternationalNumber(number)) { + PhoneNumberParser.Result.ErrorInvalidNumber -> views.phoneEntryInput.error = getString(R.string.login_msisdn_error_other) + PhoneNumberParser.Result.ErrorMissingInternationalCode -> views.phoneEntryInput.error = getString(R.string.login_msisdn_error_not_international) + is PhoneNumberParser.Result.Success -> { + val (countryCode, phoneNumber) = result + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(phoneNumber, countryCode)))) + } + } + } + + override fun onError(throwable: Throwable) { + views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt new file mode 100644 index 0000000000..41e24e96c2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.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.onboarding.ftueauth + +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.utils.colorTerminatingFullStop +import im.vector.app.databinding.FragmentFtueResetPasswordBreakerBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.themes.ThemeProvider +import im.vector.app.features.themes.ThemeUtils +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class FtueAuthResetPasswordBreakerArgument( + val email: String +) : Parcelable + +@AndroidEntryPoint +class FtueAuthResetPasswordBreakerFragment : AbstractFtueAuthFragment() { + + @Inject lateinit var themeProvider: ThemeProvider + private val params: FtueAuthResetPasswordBreakerArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordBreakerBinding { + return FragmentFtueResetPasswordBreakerBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + } + + private fun setupUi() { + views.resetPasswordBreakerGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground()) + views.resetPasswordBreakerTitle.text = getString(R.string.ftue_auth_reset_password_breaker_title) + .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) + views.resetPasswordBreakerSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) + views.resetPasswordBreakerResendEmail.debouncedClicks { viewModel.handle(OnboardingAction.ResendResetPassword) } + views.resetPasswordBreakerFooter.debouncedClicks { + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordBreakerConfirmed)) + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt new file mode 100644 index 0000000000..e8110569a2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEmailEntryFragment.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +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.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding +import im.vector.app.features.onboarding.OnboardingAction + +@AndroidEntryPoint +class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordEmailInputBinding { + return FragmentFtueResetPasswordEmailInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit, enabledPredicate = { it.isEmail() }) + views.emailEntryInput.setOnImeDoneListener { startPasswordReset() } + views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner) + views.emailEntrySubmit.debouncedClicks { startPasswordReset() } + } + + private fun startPasswordReset() { + val email = views.emailEntryInput.content() + viewModel.handle(OnboardingAction.ResetPassword(email = email, newPassword = null)) + } + + override fun onError(throwable: Throwable) { + views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt new file mode 100644 index 0000000000..6282fded61 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordEntryFragment.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.clearErrorOnChange +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueResetPasswordInputBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState + +@AndroidEntryPoint +class FtueAuthResetPasswordEntryFragment : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueResetPasswordInputBinding { + return FragmentFtueResetPasswordInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.newPasswordInput.associateContentStateWith(button = views.newPasswordSubmit) + views.newPasswordInput.setOnImeDoneListener { resetPassword() } + views.newPasswordInput.clearErrorOnChange(viewLifecycleOwner) + views.newPasswordSubmit.debouncedClicks { resetPassword() } + } + + private fun resetPassword() { + viewModel.handle( + OnboardingAction.ConfirmNewPassword( + newPassword = views.newPasswordInput.content(), + signOutAllDevices = views.entrySignOutAll.isChecked + ) + ) + } + + override fun onError(throwable: Throwable) { + views.newPasswordInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun updateWithState(state: OnboardingViewState) { + views.signedOutAllGroup.isVisible = state.resetState.supportsLogoutAllDevices + + if (state.isLoading) { + views.newPasswordInput.editText().hidePassword() + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetResetPassword) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt index adc6e1b214..956566a587 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordSuccessFragment.kt @@ -41,7 +41,7 @@ class FtueAuthResetPasswordSuccessFragment @Inject constructor() : AbstractFtueA } private fun submit() { - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone)) + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordComplete)) } override fun resetViewModel() { 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 fa37e2edce..bb8c523b5f 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 @@ -20,6 +20,7 @@ import android.content.Intent import android.os.Parcelable import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.core.view.isVisible @@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.popBackstack @@ -162,41 +162,44 @@ class FtueAuthVariant( ) is OnboardingViewEvents.OnWebLoginError -> onWebLoginError(viewEvents) is OnboardingViewEvents.OnForgetPasswordClicked -> + when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordEmailEntryFragment::class.java) + else -> addLoginStageFragmentToBackstack(FtueAuthResetPasswordFragment::class.java) + } + is OnboardingViewEvents.OnResetPasswordEmailConfirmationSent -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when { + vectorFeatures.isOnboardingCombinedLoginEnabled() -> addLoginStageFragmentToBackstack( + FtueAuthResetPasswordBreakerFragment::class.java, + FtueAuthResetPasswordBreakerArgument(viewEvents.email), + ) + else -> activity.addFragmentToBackstack( + views.loginFragmentContainer, + FtueAuthResetPasswordMailConfirmationFragment::class.java, + ) + } + } + OnboardingViewEvents.OnResetPasswordBreakerConfirmed -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.addFragmentToBackstack( views.loginFragmentContainer, - FtueAuthResetPasswordFragment::class.java, - option = commonOption - ) - is OnboardingViewEvents.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack( - views.loginFragmentContainer, - FtueAuthResetPasswordMailConfirmationFragment::class.java, + FtueAuthResetPasswordEntryFragment::class.java, option = commonOption ) } - is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - activity.addFragmentToBackstack( - views.loginFragmentContainer, - FtueAuthResetPasswordSuccessFragment::class.java, - option = commonOption - ) + is OnboardingViewEvents.OpenResetPasswordComplete -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + addLoginStageFragmentToBackstack(FtueAuthResetPasswordSuccessFragment::class.java) } - is OnboardingViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + OnboardingViewEvents.OnResetPasswordComplete -> { + Toast.makeText(activity, R.string.ftue_auth_password_reset_confirmation, Toast.LENGTH_SHORT).show() + activity.popBackstack() } is OnboardingViewEvents.OnSendEmailSuccess -> { openWaitForEmailVerification(viewEvents.email) } is OnboardingViewEvents.OnSendMsisdnSuccess -> { - // Pop the enter Msisdn Fragment - supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - addRegistrationStageFragmentToBackstack( - FtueAuthGenericTextInputFormFragment::class.java, - FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, viewEvents.msisdn), - ) + openMsisdnConfirmation(viewEvents.msisdn) } is OnboardingViewEvents.Failure, is OnboardingViewEvents.Loading -> @@ -380,12 +383,21 @@ class FtueAuthVariant( when (stage) { is Stage.ReCaptcha -> onCaptcha(stage) is Stage.Email -> onEmail(stage) - is Stage.Msisdn -> addRegistrationStageFragmentToBackstack( + is Stage.Msisdn -> onMsisdn(stage) + is Stage.Terms -> onTerms(stage) + else -> Unit // Should not happen + } + } + + private fun onMsisdn(stage: Stage) { + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthPhoneEntryFragment::class.java + ) + else -> addRegistrationStageFragmentToBackstack( FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), ) - is Stage.Terms -> onTerms(stage) - else -> Unit // Should not happen } } @@ -415,6 +427,20 @@ class FtueAuthVariant( } } + private fun openMsisdnConfirmation(msisdn: String) { + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthPhoneConfirmationFragment::class.java, + FtueAuthPhoneConfirmationFragmentArgument(msisdn), + ) + else -> addRegistrationStageFragmentToBackstack( + FtueAuthGenericTextInputFormFragment::class.java, + FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, msisdn), + ) + } + } + private fun onTerms(stage: Stage.Terms) { when { vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( @@ -496,4 +522,14 @@ class FtueAuthVariant( option = commonOption ) } + + private fun addLoginStageFragmentToBackstack(fragmentClass: Class, params: Parcelable? = null) { + activity.addFragmentToBackstack( + views.loginFragmentContainer, + fragmentClass, + params, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index 75090f2a55..4649c7c799 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -58,12 +58,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor( } private fun setupUi() { - views.emailVerificationGradientContainer.setBackgroundResource( - when (themeProvider.isLightTheme()) { - true -> R.drawable.bg_waiting_for_email_verification - false -> R.drawable.bg_color_background - } - ) + views.emailVerificationGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground()) views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title) .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt index 8d63fbf547..8deb10b7b8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt @@ -18,9 +18,11 @@ package im.vector.app.features.onboarding.ftueauth import android.widget.Button import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R import im.vector.app.core.extensions.hasContentFlow import im.vector.app.features.login.SignMode import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.themes.ThemeProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach @@ -49,3 +51,8 @@ fun observeContentChangesAndResetErrors(username: TextInputLayout, password: Tex submit.isEnabled = it } } + +fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) { + true -> R.drawable.bg_gradient_ftue_breaker + false -> R.drawable.bg_color_background +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt new file mode 100644 index 0000000000..6a46a466eb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import javax.inject.Inject + +class PhoneNumberParser @Inject constructor( + private val phoneNumberUtil: PhoneNumberUtil +) { + + fun parseInternationalNumber(rawPhoneNumber: String): Result { + return when { + rawPhoneNumber.doesNotStartWith("+") -> Result.ErrorMissingInternationalCode + else -> parseNumber(rawPhoneNumber) + } + } + + private fun parseNumber(rawPhoneNumber: String) = try { + val phoneNumber = phoneNumberUtil.parse(rawPhoneNumber, null) + Result.Success(phoneNumberUtil.getRegionCodeForCountryCode(phoneNumber.countryCode), rawPhoneNumber) + } catch (e: NumberParseException) { + Result.ErrorInvalidNumber + } + + sealed interface Result { + object ErrorMissingInternationalCode : Result + object ErrorInvalidNumber : Result + data class Success(val countryCode: String, val phoneNumber: String) : Result + } + + private fun String.doesNotStartWith(input: String) = !startsWith(input) +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt index dc56820424..24f8fc5f45 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt @@ -24,8 +24,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_splash_carousel) -abstract class SplashCarouselItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SplashCarouselItem : VectorEpoxyModel(R.layout.item_splash_carousel) { @EpoxyAttribute lateinit var item: SplashCarouselState.Item diff --git a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt index 0f01d58c96..ecbee7438a 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt @@ -19,8 +19,8 @@ package im.vector.app.features.poll import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState data class PollViewState( - val question: String, - val totalVotes: String, - val canVote: Boolean, - val optionViewStates: List?, + val question: String, + val votesStatus: String, + val canVote: Boolean, + val optionViewStates: List?, ) diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt index 8232e2ebb9..daa7f0e907 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import org.matrix.android.sdk.api.session.room.model.message.PollType -@EpoxyModelClass(layout = R.layout.item_poll_type_selection) -abstract class PollTypeSelectionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class PollTypeSelectionItem : VectorEpoxyModel(R.layout.item_poll_type_selection) { @EpoxyAttribute var pollType: PollType = PollType.DISCLOSED_UNSTABLE diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 0bef8a0e9a..d86276b9e1 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -29,6 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.databinding.ActivityBugReportBinding import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber @@ -37,7 +38,9 @@ import timber.log.Timber * Form to send a bug report. */ @AndroidEntryPoint -class BugReportActivity : VectorBaseActivity() { +class BugReportActivity : + VectorBaseActivity(), + VectorMenuProvider { override fun getBinding() = ActivityBugReportBinding.inflate(layoutInflater) @@ -123,15 +126,13 @@ class BugReportActivity : VectorBaseActivity() { override fun getMenuRes() = R.menu.bug_report - override fun onPrepareOptionsMenu(menu: Menu): Boolean { + override fun handlePrepareMenu(menu: Menu) { menu.findItem(R.id.ic_action_send_bug_report)?.let { val isValid = !views.bugReportMaskView.isVisible it.isEnabled = isValid it.icon.alpha = if (isValid) 255 else 100 } - - return super.onPrepareOptionsMenu(menu) } private fun isInternalBuild(): Boolean = BuildConfig.DEBUG || BuildConfig.GIT_BRANCH_NAME == "sm_fdroid" @@ -144,18 +145,18 @@ class BugReportActivity : VectorBaseActivity() { } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.ic_action_send_bug_report -> { if (views.bugReportEditText.text.toString().trim().length >= minBugReportLength()) { sendBugReport() } else { views.bugReportTextInputLayout.error = getString(R.string.bug_report_error_too_short) } - return true + true } + else -> false } - return super.onOptionsItemSelected(item) } /** diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index e4d3d78ea2..ce0789f870 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -21,7 +21,6 @@ import android.content.Intent import android.graphics.Typeface import android.util.TypedValue import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.core.view.isVisible @@ -33,6 +32,7 @@ import im.vector.app.EmojiCompatFontProvider import im.vector.app.R import im.vector.app.core.extensions.observeEvent import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.lib.core.utils.flow.throttleFirst @@ -48,13 +48,17 @@ import javax.inject.Inject * TODO Finish Refactor to vector base activity */ @AndroidEntryPoint -class EmojiReactionPickerActivity : VectorBaseActivity(), - EmojiCompatFontProvider.FontProviderListener { +class EmojiReactionPickerActivity : + VectorBaseActivity(), + EmojiCompatFontProvider.FontProviderListener, + VectorMenuProvider { lateinit var viewModel: EmojiChooserViewModel override fun getMenuRes() = R.menu.menu_emoji_reaction_picker + override fun handleMenuItemSelected(item: MenuItem) = false + override fun getBinding() = ActivityEmojiReactionPickerBinding.inflate(layoutInflater) override fun getCoordinatorLayout() = views.coordinatorLayout @@ -138,10 +142,7 @@ class EmojiReactionPickerActivity : VectorBaseActivity searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { @@ -175,7 +176,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { +@EpoxyModelClass +abstract class EmojiSearchResultItem : VectorEpoxyModel(R.layout.item_emoji_result) { @EpoxyAttribute lateinit var emojiItem: EmojiItem diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomItem.kt index ff1bcb6c97..e542277d4c 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomItem.kt @@ -31,8 +31,8 @@ import im.vector.app.core.platform.ButtonStateView import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_public_room) -abstract class PublicRoomItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class PublicRoomItem : VectorEpoxyModel(R.layout.item_public_room) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index fdaf523885..e16e1ec313 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentPublicRoomsBinding @@ -55,7 +56,8 @@ class PublicRoomsFragment @Inject constructor( private val permalinkHandler: PermalinkHandler, private val session: Session ) : VectorBaseFragment(), - PublicRoomsController.Callback { + PublicRoomsController.Callback, + VectorMenuProvider { private val viewModel: RoomDirectoryViewModel by activityViewModel() private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel @@ -105,14 +107,13 @@ class PublicRoomsFragment @Inject constructor( super.onDestroyView() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_room_directory_change_protocol -> { sharedActionViewModel.post(RoomDirectorySharedAction.ChangeProtocol) true } - else -> - super.onOptionsItemSelected(item) + else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt index 1383aec54b..a22c5791a3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt @@ -29,8 +29,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_unknown_room) -abstract class UnknownRoomItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UnknownRoomItem : VectorEpoxyModel(R.layout.item_unknown_room) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt index e898bb2230..f9b77ba8f1 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt @@ -32,8 +32,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.glide.GlideApp -@EpoxyModelClass(layout = R.layout.item_room_directory) -abstract class RoomDirectoryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomDirectoryItem : VectorEpoxyModel(R.layout.item_room_directory) { @EpoxyAttribute var directoryAvatarUrl: String? = null diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt index 572f732369..d65a35c200 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_room_directory_server) -abstract class RoomDirectoryServerItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomDirectoryServerItem : VectorEpoxyModel(R.layout.item_room_directory_server) { @EpoxyAttribute var serverName: String? = null 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 b82da8aa55..88a27f246c 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 @@ -42,6 +42,7 @@ import im.vector.app.core.extensions.copyOnLongClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogShareQrCodeBinding @@ -74,7 +75,8 @@ class RoomMemberProfileFragment @Inject constructor( private val roomDetailPendingActionStore: RoomDetailPendingActionStore, private val matrixItemColorProvider: MatrixItemColorProvider ) : VectorBaseFragment(), - RoomMemberProfileController.Callback { + RoomMemberProfileController.Callback, + VectorMenuProvider { private lateinit var headerViews: ViewStubRoomMemberProfileHeaderBinding @@ -160,14 +162,14 @@ class RoomMemberProfileFragment @Inject constructor( } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.roomMemberProfileShareAction -> { viewModel.handle(RoomMemberProfileAction.ShareRoomMemberProfile) - return true + true } + else -> false } - return super.onOptionsItemSelected(item) } private fun handleStartVerification(startVerification: RoomMemberProfileViewEvents.StartVerification) { 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 1b9912d2e0..2f8128b7af 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 @@ -39,6 +39,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.copyOnLongClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentMatrixProfileBinding @@ -70,7 +71,8 @@ class RoomProfileFragment @Inject constructor( private val roomDetailPendingActionStore: RoomDetailPendingActionStore, ) : VectorBaseFragment(), - RoomProfileController.Callback { + RoomProfileController.Callback, + VectorMenuProvider { private lateinit var headerViews: ViewStubRoomProfileHeaderBinding @@ -170,14 +172,14 @@ class RoomProfileFragment @Inject constructor( headerViews.roomProfileAliasView.copyOnLongClick() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.roomProfileShareAction -> { roomProfileViewModel.handle(RoomProfileAction.ShareRoomProfile) - return true + true } + else -> false } - return super.onOptionsItemSelected(item) } private fun handleQuickActions(action: RoomListQuickActionsSharedAction) = when (action) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 951e3e1dcd..52a2339f13 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.args @@ -114,6 +115,7 @@ class RoomMemberListFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { viewState -> + views.roomSettingGeneric.progressBar.isGone = viewState.areAllMembersLoaded roomMemberListController.setData(viewState) renderRoomSummary(viewState) views.inviteUsersButton.isVisible = viewState.actionsPermissions.canInvite 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 e4156916fd..ae6df8e44c 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 @@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -67,6 +68,7 @@ class RoomMemberListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! + private val roomFlow = room.flow() init { observeRoomMemberSummaries() @@ -83,8 +85,8 @@ class RoomMemberListViewModel @AssistedInject constructor( } combine( - room.flow().liveRoomMembers(roomMemberQueryParams), - room.flow() + roomFlow.liveRoomMembers(roomMemberQueryParams), + roomFlow .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) .mapOptional { it.content.toModel() } .unwrap() @@ -95,8 +97,19 @@ class RoomMemberListViewModel @AssistedInject constructor( copy(roomMemberSummaries = async) } + roomFlow.liveAreAllMembersLoaded() + .distinctUntilChanged() + .onEach { + setState { + copy( + areAllMembersLoaded = it + ) + } + } + .launchIn(viewModelScope) + if (room.roomCryptoService().isEncrypted()) { - room.flow().liveRoomMembers(roomMemberQueryParams) + roomFlow.liveRoomMembers(roomMemberQueryParams) .flatMapLatest { membersSummary -> session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) .asFlow() @@ -139,7 +152,7 @@ class RoomMemberListViewModel @AssistedInject constructor( } private fun observeRoomSummary() { - room.flow().liveRoomSummary() + roomFlow.liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) @@ -147,7 +160,7 @@ class RoomMemberListViewModel @AssistedInject constructor( } private fun observeThirdPartyInvites() { - room.flow() + roomFlow .liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE), QueryStringValue.IsNotNull) .execute { async -> copy(threePidInvites = async) 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 74e0cf2fcf..41aa24dbe6 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 @@ -32,6 +32,7 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, val roomMemberSummaries: Async = Uninitialized, + val areAllMembersLoaded: Boolean = false, val ignoredUserIds: List = emptyList(), val filter: String = "", val threePidInvites: Async> = Uninitialized, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 4f92524efc..45c8461fa7 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -36,6 +36,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast @@ -64,7 +65,8 @@ class RoomSettingsFragment @Inject constructor( VectorBaseFragment(), RoomSettingsController.Callback, OnBackPressed, - GalleryOrCameraDialogHelper.Listener { + GalleryOrCameraDialogHelper.Listener, + VectorMenuProvider { private val viewModel: RoomSettingsViewModel by fragmentViewModel() private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel @@ -139,18 +141,20 @@ class RoomSettingsFragment @Inject constructor( super.onDestroyView() } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { withState(viewModel) { state -> menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction } - super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.roomSettingsSaveAction) { - viewModel.handle(RoomSettingsAction.Save) + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.roomSettingsSaveAction -> { + viewModel.handle(RoomSettingsAction.Save) + true + } + else -> false } - return super.onOptionsItemSelected(item) } override fun invalidate() = withState(viewModel) { viewState -> diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt index 382ef1c545..eaba1a5fef 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt @@ -33,8 +33,8 @@ import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_joinrule_restricted) -abstract class SpaceJoinRuleItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceJoinRuleItem : VectorEpoxyModel(R.layout.item_bottom_sheet_joinrule_restricted) { @EpoxyAttribute var selected: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileItem.kt index 6b75209224..eeef076312 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/UploadsFileItem.kt @@ -25,8 +25,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_uploads_file) -abstract class UploadsFileItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UploadsFileItem : VectorEpoxyModel(R.layout.item_uploads_file) { @EpoxyAttribute var title: String? = null @EpoxyAttribute var subtitle: String? = null diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsImageItem.kt index c4ee807b06..12be88ffa6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsImageItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsImageItem.kt @@ -27,8 +27,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.features.media.ImageContentRenderer -@EpoxyModelClass(layout = R.layout.item_uploads_image) -abstract class UploadsImageItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UploadsImageItem : VectorEpoxyModel(R.layout.item_uploads_image) { @EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer @EpoxyAttribute lateinit var data: ImageContentRenderer.Data diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsVideoItem.kt index e2f0d6eee9..fdc69f0d8f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsVideoItem.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer -@EpoxyModelClass(layout = R.layout.item_uploads_video) -abstract class UploadsVideoItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UploadsVideoItem : VectorEpoxyModel(R.layout.item_uploads_video) { @EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer @EpoxyAttribute lateinit var data: VideoContentRenderer.Data 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 3bfc5f0ae5..8097b353da 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 @@ -168,7 +168,6 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" - const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN" const val SETTINGS_LABS_AUTO_REPORT_UISI = "SETTINGS_LABS_AUTO_REPORT_UISI" const val SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME = "SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME" @@ -178,6 +177,8 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" private const val SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY = "SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY" + const val SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY = "SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY" + // SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM" const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB" @@ -1271,22 +1272,12 @@ class VectorPreferences @Inject constructor( } } - private fun labsSpacesOnlyOrphansInHome(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false) - } - fun labsAutoReportUISI(): Boolean { return defaultPrefs.getBoolean(SETTINGS_LABS_AUTO_REPORT_UISI, false) } fun prefSpacesShowAllRoomInHome(): Boolean { - return defaultPrefs.getBoolean( - SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, - true - // migration of old property - leads to unexpected results, since we don't do the same in the xml - // - and who cares nowadays either way - //!labsSpacesOnlyOrphansInHome() - ) + return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, true) } /* diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 10860aaba1..8f81704629 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -21,6 +21,7 @@ import android.os.Bundle import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.preference.Preference +import androidx.preference.SwitchPreference import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.preference.VectorSwitchPreference @@ -85,6 +86,17 @@ class VectorSettingsLabsFragment @Inject constructor( false } } + + findPreference(VectorPreferences.SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY)?.let { pref -> + // ensure correct default + pref.isChecked = session.cryptoService().isShareKeysOnInviteEnabled() + + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + session.cryptoService().enableShareKeyOnInvite(pref.isChecked) + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) + true + } + } } /** diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt index b0f6261424..6486b8a3ca 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt @@ -37,8 +37,8 @@ import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo /** * A list item for Device. */ -@EpoxyModelClass(layout = R.layout.item_device) -abstract class DeviceItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class DeviceItem : VectorEpoxyModel(R.layout.item_device) { @EpoxyAttribute lateinit var deviceInfo: DeviceInfo diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index 4721fa7bfb..5684e941f1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -26,8 +26,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE import com.airbnb.mvrx.Loading import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -36,6 +34,7 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.safeOpenOutputStream import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.time.Clock import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding @@ -44,7 +43,8 @@ import javax.inject.Inject class KeyRequestsFragment @Inject constructor( private val clock: Clock, -) : VectorBaseFragment() { +) : VectorBaseFragment(), + VectorMenuProvider { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDevtoolKeyrequestsBinding { return FragmentDevtoolKeyrequestsBinding.inflate(inflater, container, false) @@ -61,19 +61,6 @@ class KeyRequestsFragment @Inject constructor( override fun getMenuRes(): Int = R.menu.menu_audit - private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - invalidateOptionsMenu() - } - - override fun onPageScrollStateChanged(state: Int) { - childFragmentManager.fragments.forEach { - it.setHasOptionsMenu(state == SCROLL_STATE_IDLE) - } - invalidateOptionsMenu() - } - } - override fun invalidate() = withState(viewModel) { when (it.exporting) { is Loading -> views.exportWaitingView.isVisible = true @@ -81,16 +68,10 @@ class KeyRequestsFragment @Inject constructor( } } - override fun onDestroy() { - invalidateOptionsMenu() - super.onDestroy() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) mPagerAdapter = KeyReqPagerAdapter(this) views.devToolKeyRequestPager.adapter = mPagerAdapter - views.devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener) TabLayoutMediator(views.devToolKeyRequestTabs, views.devToolKeyRequestPager) { tab, position -> when (position) { @@ -119,25 +100,26 @@ class KeyRequestsFragment @Inject constructor( } override fun onDestroyView() { - views.devToolKeyRequestPager.unregisterOnPageChangeCallback(pageAdapterListener) mPagerAdapter = null super.onDestroyView() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.audit_export) { - selectTxtFileToWrite( - activity = requireActivity(), - activityResultLauncher = epxortAuditForActivityResult, - defaultFileName = "audit-export_${clock.epochMillis()}.txt", - chooserHint = "Export Audit" - ) - return true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.audit_export -> { + selectTxtFileToWrite( + activity = requireActivity(), + activityResultLauncher = exportAuditForActivityResult, + defaultFileName = "audit-export_${clock.epochMillis()}.txt", + chooserHint = "Export Audit" + ) + true + } + else -> false } - return super.onOptionsItemSelected(item) } - private val epxortAuditForActivityResult = registerStartForActivityResult { activityResult -> + private val exportAuditForActivityResult = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { val uri = activityResult.data?.data if (uri != null) { diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/UserItem.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/UserItem.kt index bb27a8be36..ec02148aca 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/UserItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/UserItem.kt @@ -32,8 +32,8 @@ import org.matrix.android.sdk.api.util.MatrixItem /** * A list item for User. */ -@EpoxyModelClass(layout = R.layout.item_user) -abstract class UserItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UserItem : VectorEpoxyModel(R.layout.item_user) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocaleItem.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocaleItem.kt index c74fb7c5cf..2478ae4cea 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocaleItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocaleItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_locale) -abstract class LocaleItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LocaleItem : VectorEpoxyModel(R.layout.item_locale) { @EpoxyAttribute var title: String? = null @EpoxyAttribute var subtitle: String? = null diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt index 03eb581b7f..5ef6e02330 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt @@ -20,14 +20,14 @@ import android.view.View import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide import org.matrix.android.sdk.api.session.pushers.Pusher -@EpoxyModelClass(layout = R.layout.item_pushgateway) -abstract class PushGatewayItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class PushGatewayItem : VectorEpoxyModel(R.layout.item_pushgateway) { @EpoxyAttribute lateinit var pusher: Pusher diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt index 82462d7cc0..da06f067c6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt @@ -29,6 +29,7 @@ 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.VectorMenuProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding import org.matrix.android.sdk.api.session.pushers.Pusher import javax.inject.Inject @@ -36,7 +37,8 @@ import javax.inject.Inject // Referenced in vector_settings_notifications.xml class PushGatewaysFragment @Inject constructor( private val epoxyController: PushGateWayController -) : VectorBaseFragment() { +) : VectorBaseFragment(), + VectorMenuProvider { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { return FragmentGenericRecyclerBinding.inflate(inflater, container, false) @@ -46,14 +48,13 @@ class PushGatewaysFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_push_gateways - override fun onOptionsItemSelected(item: MenuItem): Boolean { + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.refresh -> { viewModel.handle(PushGatewayAction.Refresh) true } - else -> - super.onOptionsItemSelected(item) + else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt index d56562a7dd..5a1dd055bd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRuleItem.kt @@ -25,16 +25,16 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.notifications.toNotificationAction import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.session.pushrules.getActions import org.matrix.android.sdk.api.session.pushrules.rest.PushRule -@EpoxyModelClass(layout = R.layout.item_pushrule_raw) -abstract class PushRuleItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class PushRuleItem : VectorEpoxyModel(R.layout.item_pushrule_raw) { @EpoxyAttribute lateinit var pushRule: PushRule diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt index 814651dd04..4027b0adf2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidItem.kt @@ -22,14 +22,14 @@ import androidx.annotation.DrawableRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_settings_three_pid) -abstract class ThreePidItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class ThreePidItem : VectorEpoxyModel(R.layout.item_settings_three_pid) { @EpoxyAttribute var title: String? = null diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index a113c8105d..3f8923dd68 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -116,7 +116,6 @@ class IncomingShareFragment @Inject constructor( } private fun handleIncomingShareIntent(intent: Intent) = shareIntentHandler.handleIncomingShareIntent( - requireContext(), intent, onFile = { val sharedData = SharedData.Attachments(it) diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 1214ac6819..265cf3199e 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -18,6 +18,7 @@ package im.vector.app.features.signout.soft import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized @@ -35,6 +36,7 @@ import im.vector.app.features.signout.soft.epoxy.loginRedButtonItem import im.vector.app.features.signout.soft.epoxy.loginTextItem import im.vector.app.features.signout.soft.epoxy.loginTitleItem import im.vector.app.features.signout.soft.epoxy.loginTitleSmallItem +import org.matrix.android.sdk.api.auth.LoginType import javax.inject.Inject class SoftLogoutController @Inject constructor( @@ -91,55 +93,76 @@ class SoftLogoutController @Inject constructor( } } - private fun buildForm(state: SoftLogoutViewState) { + private fun buildForm(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest) { + is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error) + is Success -> buildLoginSuccessItem(state) + is Loading, Uninitialized -> buildLoadingItem() + is Incomplete -> Unit + } + + private fun buildLoadingItem() { + loadingItem { + id("loading") + } + } + + private fun buildLoginErrorWithRetryItem(error: Throwable) { val host = this - when (state.asyncHomeServerLoginFlowRequest) { - Uninitialized, - is Loading -> { - loadingItem { - id("loading") - } - } - is Fail -> { - loginErrorWithRetryItem { - id("errorRetry") - text(host.errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) - listener { host.listener?.retry() } - } - } - is Success -> { - when (state.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Password -> { - loginPasswordFormItem { - id("passwordForm") - stringProvider(host.stringProvider) - passwordValue(state.enteredPassword) - submitEnabled(state.enteredPassword.isNotEmpty()) - onPasswordEdited { host.listener?.passwordEdited(it) } - errorText((state.asyncLoginAction as? Fail)?.error?.let { host.errorFormatter.toHumanReadable(it) }) - forgetPasswordClickListener { host.listener?.forgetPasswordClicked() } - submitClickListener { host.listener?.submit() } - } - } - is LoginMode.Sso -> { - loginCenterButtonItem { - id("sso") - text(host.stringProvider.getString(R.string.login_signin_sso)) - listener { host.listener?.signinFallbackSubmit() } - } - } - is LoginMode.SsoAndPassword -> { - } - LoginMode.Unsupported -> { - loginCenterButtonItem { - id("fallback") - text(host.stringProvider.getString(R.string.login_signin)) - listener { host.listener?.signinFallbackSubmit() } - } - } - LoginMode.Unknown -> Unit // Should not happen - } - } + loginErrorWithRetryItem { + id("errorRetry") + text(host.errorFormatter.toHumanReadable(error)) + listener { host.listener?.retry() } + } + } + + private fun buildLoginSuccessItem(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest.invoke()) { + LoginMode.Password -> buildLoginPasswordForm(state) + is LoginMode.Sso -> buildLoginSSOForm() + is LoginMode.SsoAndPassword -> disambiguateLoginSSOAndPasswordForm(state) + LoginMode.Unsupported -> buildLoginUnsupportedForm() + LoginMode.Unknown, null -> Unit // Should not happen + } + + private fun buildLoginPasswordForm(state: SoftLogoutViewState) { + val host = this + loginPasswordFormItem { + id("passwordForm") + stringProvider(host.stringProvider) + passwordValue(state.enteredPassword) + submitEnabled(state.enteredPassword.isNotEmpty()) + onPasswordEdited { host.listener?.passwordEdited(it) } + errorText((state.asyncLoginAction as? Fail)?.error?.let { host.errorFormatter.toHumanReadable(it) }) + forgetPasswordClickListener { host.listener?.forgetPasswordClicked() } + submitClickListener { host.listener?.submit() } + } + } + + private fun buildLoginSSOForm() { + val host = this + loginCenterButtonItem { + id("sso") + text(host.stringProvider.getString(R.string.login_signin_sso)) + listener { host.listener?.signinFallbackSubmit() } + } + } + + private fun disambiguateLoginSSOAndPasswordForm(state: SoftLogoutViewState) { + when (state.loginType) { + LoginType.PASSWORD -> buildLoginPasswordForm(state) + LoginType.SSO -> buildLoginSSOForm() + LoginType.DIRECT, + LoginType.CUSTOM, + LoginType.UNSUPPORTED -> buildLoginUnsupportedForm() + LoginType.UNKNOWN -> Unit + } + } + + private fun buildLoginUnsupportedForm() { + val host = this + loginCenterButtonItem { + id("fallback") + text(host.stringProvider.getString(R.string.login_signin)) + listener { host.listener?.signinFallbackSubmit() } } } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 34d001caad..9d0580638b 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -35,14 +35,12 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.LoginMode import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getUser import timber.log.Timber -/** - * TODO Test push: disable the pushers? - */ class SoftLogoutViewModel @AssistedInject constructor( @Assisted initialState: SoftLogoutViewState, private val session: Session, @@ -70,7 +68,8 @@ class SoftLogoutViewModel @AssistedInject constructor( userId = userId, deviceId = session.sessionParams.deviceId.orEmpty(), userDisplayName = session.getUser(userId)?.displayName ?: userId, - hasUnsavedKeys = session.hasUnsavedKeys() + hasUnsavedKeys = session.hasUnsavedKeys(), + loginType = session.sessionParams.loginType, ) } else { SoftLogoutViewState( @@ -78,7 +77,8 @@ class SoftLogoutViewModel @AssistedInject constructor( userId = "", deviceId = "", userDisplayName = "", - hasUnsavedKeys = false + hasUnsavedKeys = false, + loginType = LoginType.UNKNOWN, ) } } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt index 511711ab2f..28c8273412 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.features.login.LoginMode +import org.matrix.android.sdk.api.auth.LoginType data class SoftLogoutViewState( val asyncHomeServerLoginFlowRequest: Async = Uninitialized, @@ -31,7 +32,8 @@ data class SoftLogoutViewState( val deviceId: String, val userDisplayName: String, val hasUnsavedKeys: Boolean, - val enteredPassword: String = "" + val loginType: LoginType, + val enteredPassword: String = "", ) : MavericksState { fun isLoading(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginCenterButtonItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginCenterButtonItem.kt index f3e6378a4b..60cf34c2c2 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginCenterButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginCenterButtonItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_login_centered_button) -abstract class LoginCenterButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginCenterButtonItem : VectorEpoxyModel(R.layout.item_login_centered_button) { @EpoxyAttribute var text: String? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginErrorWithRetryItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginErrorWithRetryItem.kt index 834fedb272..e190a41823 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginErrorWithRetryItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginErrorWithRetryItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_login_error_retry) -abstract class LoginErrorWithRetryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginErrorWithRetryItem : VectorEpoxyModel(R.layout.item_login_error_retry) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginHeaderItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginHeaderItem.kt index 0ce50b9cdb..904835a9c5 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginHeaderItem.kt @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_login_header) -abstract class LoginHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginHeaderItem : VectorEpoxyModel(R.layout.item_login_header) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginPasswordFormItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginPasswordFormItem.kt index c032da7192..ac65e6f641 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginPasswordFormItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginPasswordFormItem.kt @@ -35,8 +35,8 @@ import im.vector.app.core.epoxy.setValueOnce import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.resources.StringProvider -@EpoxyModelClass(layout = R.layout.item_login_password_form) -abstract class LoginPasswordFormItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginPasswordFormItem : VectorEpoxyModel(R.layout.item_login_password_form) { @EpoxyAttribute var passwordValue: String = "" @EpoxyAttribute var submitEnabled: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginRedButtonItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginRedButtonItem.kt index 981de3c962..3febdc20b6 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginRedButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginRedButtonItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_login_red_button) -abstract class LoginRedButtonItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginRedButtonItem : VectorEpoxyModel(R.layout.item_login_red_button) { @EpoxyAttribute var text: String? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTextItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTextItem.kt index f2727379ab..b036b2d77a 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTextItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_login_text) -abstract class LoginTextItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginTextItem : VectorEpoxyModel(R.layout.item_login_text) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleItem.kt index dcb565b490..d2627590d8 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_login_title) -abstract class LoginTitleItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginTitleItem : VectorEpoxyModel(R.layout.item_login_title) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleSmallItem.kt b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleSmallItem.kt index 04f5ca0a78..796a271b28 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleSmallItem.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/epoxy/LoginTitleSmallItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_login_title_small) -abstract class LoginTitleSmallItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class LoginTitleSmallItem : VectorEpoxyModel(R.layout.item_login_title_small) { @EpoxyAttribute var text: String? = null diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceAddItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceAddItem.kt index f25de9fe99..02bb702fae 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceAddItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceAddItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_space_add) -abstract class SpaceAddItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceAddItem : VectorEpoxyModel(R.layout.item_space_add) { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceBetaHeaderItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceBetaHeaderItem.kt index 667f895d4d..8cf3766c73 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceBetaHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceBetaHeaderItem.kt @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_space_beta_header) -abstract class SpaceBetaHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceBetaHeaderItem : VectorEpoxyModel(R.layout.item_space_beta_header) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 4ca7d303ee..dae43c5953 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -36,8 +36,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_space) -abstract class SpaceSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceSummaryItem : VectorEpoxyModel(R.layout.item_space) { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt index 4a36bd50fe..f50c0590af 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt @@ -34,8 +34,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_sub_space) -abstract class SubSpaceSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SubSpaceSummaryItem : VectorEpoxyModel(R.layout.item_sub_space) { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index 4cbbbc876d..3818f4278a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -38,6 +38,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed 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.utils.colorizeMatchingText import im.vector.app.core.utils.isValidUrl @@ -67,7 +68,8 @@ class SpaceDirectoryFragment @Inject constructor( ) : VectorBaseFragment(), SpaceDirectoryController.InteractionListener, TimelineEventController.UrlClickCallback, - OnBackPressed { + OnBackPressed, + VectorMenuProvider { override fun getMenuRes() = R.menu.menu_space_directory @@ -177,40 +179,41 @@ class SpaceDirectoryFragment @Inject constructor( views.addOrCreateChatRoomButton.isVisible = state.canAddRooms } - override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> - menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms - menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented + override fun handlePrepareMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms + menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented - menu.findItem(R.id.spaceSearch)?.let { searchItem -> - val searchView = searchItem.actionView as SearchView - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - return true - } + menu.findItem(R.id.spaceSearch)?.let { searchItem -> + val searchView = searchItem.actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return true + } - override fun onQueryTextChange(newText: String?): Boolean { - onFilterQueryChanged(newText) - return true - } - }) + override fun onQueryTextChange(newText: String?): Boolean { + onFilterQueryChanged(newText) + return true + } + }) + } } - super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { R.id.spaceAddRoom -> { withState(viewModel) { state -> addExistingRooms(state.spaceId) } - return true + true } R.id.spaceCreateRoom -> { // not implemented yet - return true + true } + else -> false } - return super.onOptionsItemSelected(item) } override fun onFilterQueryChanged(query: String?) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt index 071dadb3b4..de1273b8d5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt @@ -32,6 +32,7 @@ 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.VectorMenuProvider import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -40,7 +41,8 @@ import javax.inject.Inject class SpaceLeaveAdvancedFragment @Inject constructor( val controller: SelectChildrenController ) : VectorBaseFragment(), - SelectChildrenController.Listener { + SelectChildrenController.Listener, + VectorMenuProvider { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceLeaveAdvancedBinding.inflate(layoutInflater, container, false) @@ -49,6 +51,8 @@ class SpaceLeaveAdvancedFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_space_leave + override fun handleMenuItemSelected(item: MenuItem) = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -89,7 +93,7 @@ class SpaceLeaveAdvancedFragment @Inject constructor( } } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { menu.findItem(R.id.menu_space_leave_search)?.let { searchItem -> searchItem.bind( onExpanded = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = true)) }, @@ -97,7 +101,6 @@ class SpaceLeaveAdvancedFragment @Inject constructor( onTextChanged = { viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it)) } ) } - super.onPrepareOptionsMenu(menu) } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt index 21da4f0dfe..b1ccc1aa59 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt @@ -31,8 +31,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_room_to_manage_in_space) -abstract class RoomManageSelectionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomManageSelectionItem : VectorEpoxyModel(R.layout.item_room_to_manage_in_space) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt index df736bebbb..9d101bdb5f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt @@ -30,8 +30,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_room_to_add_in_space) -abstract class RoomSelectionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomSelectionItem : VectorEpoxyModel(R.layout.item_room_to_add_in_space) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionPlaceHolderItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionPlaceHolderItem.kt index 7c116811b4..46aa655a8d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionPlaceHolderItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionPlaceHolderItem.kt @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_room_to_add_in_space_placeholder) -abstract class RoomSelectionPlaceHolderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomSelectionPlaceHolderItem : VectorEpoxyModel(R.layout.item_room_to_add_in_space_placeholder) { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt index 28d33bd1bf..848c17deb6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt @@ -36,6 +36,7 @@ import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.databinding.FragmentSpaceAddRoomsBinding import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.debounce @@ -51,7 +52,9 @@ class SpaceAddRoomFragment @Inject constructor( private val roomEpoxyController: AddRoomListController, private val dmEpoxyController: AddRoomListController, ) : VectorBaseFragment(), - OnBackPressed, AddRoomListController.Listener { + OnBackPressed, + AddRoomListController.Listener, + VectorMenuProvider { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceAddRoomsBinding.inflate(layoutInflater, container, false) @@ -151,17 +154,18 @@ class SpaceAddRoomFragment @Inject constructor( } } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) + override fun handlePrepareMenu(menu: Menu) { menu.findItem(R.id.spaceAddRoomSaveItem).isVisible = saveNeeded } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.spaceAddRoomSaveItem) { - viewModel.handle(SpaceAddRoomActions.Save) - return true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.spaceAddRoomSaveItem -> { + viewModel.handle(SpaceAddRoomActions.Save) + true + } + else -> false } - return super.onOptionsItemSelected(item) } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index b99fe0f025..eb1de4fe60 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.time.Clock import im.vector.app.core.utils.toast @@ -66,7 +67,8 @@ class SpaceSettingsFragment @Inject constructor( ) : VectorBaseFragment(), SpaceSettingsController.Callback, GalleryOrCameraDialogHelper.Listener, - OnBackPressed { + OnBackPressed, + VectorMenuProvider { private val viewModel: RoomSettingsViewModel by fragmentViewModel() private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel() @@ -111,18 +113,20 @@ class SpaceSettingsFragment @Inject constructor( super.onDestroyView() } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { withState(viewModel) { state -> menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction } - super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.roomSettingsSaveAction) { - viewModel.handle(RoomSettingsAction.Save) + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.roomSettingsSaveAction -> { + viewModel.handle(RoomSettingsAction.Save) + true + } + else -> false } - return super.onOptionsItemSelected(item) } private fun renderRoomSummary(state: RoomSettingsViewState) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index 3f60166ba3..1181ccfa52 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -32,8 +33,6 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding import im.vector.app.features.roomprofile.members.RoomMemberListAction import im.vector.app.features.roomprofile.members.RoomMemberListViewModel @@ -45,8 +44,6 @@ import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpacePeopleFragment @Inject constructor( - private val drawableProvider: DrawableProvider, - private val colorProvider: ColorProvider, private val epoxyController: SpacePeopleListController ) : VectorBaseFragment(), OnBackPressed, SpacePeopleListController.InteractionListener { @@ -64,6 +61,7 @@ class SpacePeopleFragment @Inject constructor( } override fun invalidate() = withState(membersViewModel) { memberListState -> + views.progressBar.isGone = memberListState.areAllMembersLoaded val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 toolbar?.subtitle = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt index bf28618c6c..263cc3ef1d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/RoomChildItem.kt @@ -27,8 +27,8 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_space_roomchild) -abstract class RoomChildItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class RoomChildItem : VectorEpoxyModel(R.layout.item_space_roomchild) { @EpoxyAttribute lateinit var roomId: String diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt index c357fb14b3..eee37a585f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTopSummaryItem.kt @@ -24,8 +24,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_space_top_summary) -abstract class SpaceTopSummaryItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SpaceTopSummaryItem : VectorEpoxyModel(R.layout.item_space_top_summary) { @EpoxyAttribute var topic: String? = null diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt index 1856edb61f..b1837be9c6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_space_subspace) -abstract class SubSpaceItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class SubSpaceItem : VectorEpoxyModel(R.layout.item_space_subspace) { @EpoxyAttribute lateinit var roomId: String diff --git a/vector/src/main/java/im/vector/app/features/terms/TermItem.kt b/vector/src/main/java/im/vector/app/features/terms/TermItem.kt index b292c5b119..125139a8ca 100644 --- a/vector/src/main/java/im/vector/app/features/terms/TermItem.kt +++ b/vector/src/main/java/im/vector/app/features/terms/TermItem.kt @@ -21,14 +21,14 @@ import android.widget.CompoundButton import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick -@EpoxyModelClass(layout = R.layout.item_tos) -abstract class TermItem : EpoxyModelWithHolder() { +@EpoxyModelClass +abstract class TermItem : VectorEpoxyModel(R.layout.item_tos) { @EpoxyAttribute var checked: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt index a4c71c8cb9..fcf117ba88 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_contact_action) -abstract class ActionItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ActionItem : VectorEpoxyModel(R.layout.item_contact_action) { @EpoxyAttribute var title: String? = null @EpoxyAttribute @DrawableRes var actionIconRes: Int? = null diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt index 8d2e80d4db..3fffb1ec31 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -@EpoxyModelClass(layout = R.layout.item_contact_detail) -abstract class ContactDetailItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ContactDetailItem : VectorEpoxyModel(R.layout.item_contact_detail) { @EpoxyAttribute lateinit var threePid: String @EpoxyAttribute var matrixId: String? = null diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt index d9f424d961..ea5101d413 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt @@ -26,8 +26,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_contact_main) -abstract class ContactItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class ContactItem : VectorEpoxyModel(R.layout.item_contact_main) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var mappedContact: MappedContact diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt index 2258239bde..eaeec35791 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt @@ -27,8 +27,8 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_invite_by_mail) -abstract class InviteByEmailItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class InviteByEmailItem : VectorEpoxyModel(R.layout.item_invite_by_mail) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var foundItem: ThreePidUser diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryLetterHeaderItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryLetterHeaderItem.kt index 63f68a1f8b..78de9d7925 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryLetterHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryLetterHeaderItem.kt @@ -23,8 +23,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_user_directory_letter_header) -abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel(R.layout.item_user_directory_letter_header) { @EpoxyAttribute var letter: String = "" diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryUserItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryUserItem.kt index 42d9b8892c..12e1cd85ed 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryUserItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryUserItem.kt @@ -32,8 +32,8 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_known_user) -abstract class UserDirectoryUserItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UserDirectoryUserItem : VectorEpoxyModel(R.layout.item_known_user) { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index e893fdf0f3..b31833e37c 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -23,7 +23,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.ScrollView -import androidx.core.view.forEach import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel @@ -37,6 +36,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setupAsSearch import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.core.utils.startSharePlainTextIntent @@ -55,7 +55,8 @@ class UserListFragment @Inject constructor( private val userListController: UserListController, private val dimensionConverter: DimensionConverter, ) : VectorBaseFragment(), - UserListController.Callback { + UserListController.Callback, + VectorMenuProvider { private val args: UserListFragmentArgs by args() private val viewModel: UserListViewModel by activityViewModel() @@ -113,19 +114,24 @@ class UserListFragment @Inject constructor( super.onDestroyView() } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun handlePrepareMenu(menu: Menu) { + if (args.submitMenuItemId == -1) return withState(viewModel) { val showMenuItem = it.pendingSelections.isNotEmpty() - menu.forEach { menuItem -> - menuItem.isVisible = showMenuItem - } + menu.findItem(args.submitMenuItemId).isVisible = showMenuItem } - super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { - sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingSelections)) - return@withState true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + args.submitMenuItemId -> { + withState(viewModel) { + sharedActionViewModel.post(UserListSharedAction.OnMenuItemSubmitClick(it.pendingSelections)) + } + true + } + else -> false + } } private fun setupRecyclerView() { diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt index 795d45272c..d6e55c29ae 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt @@ -23,6 +23,7 @@ import kotlinx.parcelize.Parcelize data class UserListFragmentArgs( val title: String, val menuResId: Int, + val submitMenuItemId: Int, val excludedUserIds: Set? = null, val singleSelection: Boolean = false, val showInviteActions: Boolean = true, diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt index 82fa4a4d6f..bdc2a385e8 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt @@ -23,8 +23,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_user_list_header) -abstract class UserListHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass +abstract class UserListHeaderItem : VectorEpoxyModel(R.layout.item_user_list_header) { @EpoxyAttribute var header: String = "" diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt index fca771793b..fb63b05e2f 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction sealed class UserListSharedAction : VectorSharedAction { object Close : UserListSharedAction() object GoBack : UserListSharedAction() - data class OnMenuItemSelected(val itemId: Int, val selections: Set) : UserListSharedAction() + data class OnMenuItemSubmitClick(val selections: Set) : UserListSharedAction() object OpenPhoneBook : UserListSharedAction() object AddByQrCode : UserListSharedAction() } 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 77bdbc46ec..ad0e5a5931 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -23,8 +23,8 @@ import android.media.MediaRecorder import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.os.Build -import im.vector.opusencoder.OggOpusEncoder -import im.vector.opusencoder.configuration.SampleRate +import io.element.android.opusencoder.OggOpusEncoder +import io.element.android.opusencoder.configuration.SampleRate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive 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 d24e7fcc8c..28693ca287 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -18,14 +18,16 @@ package im.vector.app.features.voice import android.content.Context import android.os.Build +import im.vector.app.features.VectorFeatures import kotlinx.coroutines.Dispatchers import javax.inject.Inject class VoiceRecorderProvider @Inject constructor( private val context: Context, + private val vectorFeatures: VectorFeatures, ) { fun provideVoiceRecorder(): VoiceRecorder { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && vectorFeatures.forceUsageOfOpusEncoder().not()) { VoiceRecorderQ(context) } else { VoiceRecorderL(context, Dispatchers.IO) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 0c2df7856f..954f622801 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -64,8 +64,6 @@ class WidgetActivity : VectorBaseActivity() { override fun getBinding() = ActivityWidgetBinding.inflate(layoutInflater) - override fun getMenuRes() = R.menu.menu_widget - override fun getTitleRes() = R.string.room_widget_activity_title override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index cbd4b8e1ee..5501031e92 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -42,6 +42,7 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.databinding.FragmentRoomWidgetBinding import im.vector.app.features.webview.WebEventListener @@ -68,7 +69,8 @@ class WidgetFragment @Inject constructor( ) : VectorBaseFragment(), WebEventListener, - OnBackPressed { + OnBackPressed, + VectorMenuProvider { private val fragmentArgs: WidgetArgs by args() private val viewModel: WidgetViewModel by activityViewModel() @@ -79,7 +81,6 @@ class WidgetFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setHasOptionsMenu(true) views.widgetWebView.setupForWidget(this) if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(views.widgetWebView) @@ -136,53 +137,64 @@ class WidgetFragment @Inject constructor( } } - override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> - val widget = state.asyncWidget() - menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER - if (widget == null) { - menu.findItem(R.id.action_refresh)?.isVisible = false - menu.findItem(R.id.action_widget_open_ext)?.isVisible = false - menu.findItem(R.id.action_delete)?.isVisible = false - menu.findItem(R.id.action_revoke)?.isVisible = false - } else { - menu.findItem(R.id.action_refresh)?.isVisible = true - menu.findItem(R.id.action_widget_open_ext)?.isVisible = true - menu.findItem(R.id.action_delete)?.isVisible = state.canManageWidgets && widget.isAddedByMe - menu.findItem(R.id.action_revoke)?.isVisible = state.status == WidgetStatus.WIDGET_ALLOWED && !widget.isAddedByMe + override fun getMenuRes() = R.menu.menu_widget + + override fun handlePrepareMenu(menu: Menu) { + withState(viewModel) { state -> + val widget = state.asyncWidget() + menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER + if (widget == null) { + menu.findItem(R.id.action_refresh)?.isVisible = false + menu.findItem(R.id.action_widget_open_ext)?.isVisible = false + menu.findItem(R.id.action_delete)?.isVisible = false + menu.findItem(R.id.action_revoke)?.isVisible = false + } else { + menu.findItem(R.id.action_refresh)?.isVisible = true + menu.findItem(R.id.action_widget_open_ext)?.isVisible = true + menu.findItem(R.id.action_delete)?.isVisible = state.canManageWidgets && widget.isAddedByMe + menu.findItem(R.id.action_revoke)?.isVisible = state.status == WidgetStatus.WIDGET_ALLOWED && !widget.isAddedByMe + } } - super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state -> - when (item.itemId) { - R.id.action_edit -> { - navigator.openIntegrationManager( - requireContext(), - integrationManagerActivityResultLauncher, - state.roomId, - state.widgetId, - state.widgetKind.screenId - ) - return@withState true - } - R.id.action_delete -> { - deleteWidget() - return@withState true - } - R.id.action_refresh -> if (state.formattedURL.complete) { - views.widgetWebView.reload() - return@withState true - } - R.id.action_widget_open_ext -> if (state.formattedURL.complete) { - openUrlInExternalBrowser(requireContext(), state.formattedURL.invoke()) - return@withState true - } - R.id.action_revoke -> if (state.status == WidgetStatus.WIDGET_ALLOWED) { - revokeWidget() - return@withState true + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return withState(viewModel) { state -> + return@withState when (item.itemId) { + R.id.action_edit -> { + navigator.openIntegrationManager( + requireContext(), + integrationManagerActivityResultLauncher, + state.roomId, + state.widgetId, + state.widgetKind.screenId + ) + true + } + R.id.action_delete -> { + deleteWidget() + true + } + R.id.action_refresh -> { + if (state.formattedURL.complete) { + views.widgetWebView.reload() + } + true + } + R.id.action_widget_open_ext -> { + if (state.formattedURL.complete) { + openUrlInExternalBrowser(requireContext(), state.formattedURL.invoke()) + } + true + } + R.id.action_revoke -> { + if (state.status == WidgetStatus.WIDGET_ALLOWED) { + revokeWidget() + } + true + } + else -> false } } - return@withState super.onOptionsItemSelected(item) } override fun onBackPressed(toolbarButton: Boolean): Boolean = withState(viewModel) { state -> diff --git a/vector/src/main/res/drawable/ic_ftue_phone.xml b/vector/src/main/res/drawable/ic_ftue_phone.xml new file mode 100644 index 0000000000..53884d6d96 --- /dev/null +++ b/vector/src/main/res/drawable/ic_ftue_phone.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/drawable/ic_new_password.xml b/vector/src/main/res/drawable/ic_new_password.xml new file mode 100644 index 0000000000..b6171642c8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_new_password.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml index 8037f207fc..d50fdb6394 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -170,7 +170,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:hint="@string/login_signup_password_hint" - app:layout_constraintBottom_toTopOf="@id/actionSpacing" + app:layout_constraintBottom_toTopOf="@id/loginForgotPassword" app:layout_constraintEnd_toEndOf="@id/loginGutterEnd" app:layout_constraintStart_toStartOf="@id/loginGutterStart" app:layout_constraintTop_toBottomOf="@id/entrySpacing"> @@ -184,13 +184,27 @@ +