diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index 9cd33143ad..8fe51eb8d5 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -325,5 +325,5 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }} - text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" - html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" + text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" + html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a97d532644..014139d0ba 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -5,6 +5,11 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx4g + jobs: check: name: Project Check Suite @@ -97,6 +102,25 @@ jobs: comment_id: ${{ steps.fc.outputs.comment-id }} }) +# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin + dependency-analysis: + name: Dependency analysis + runs-on: ubuntu-latest + # Allow all jobs on main and develop. Just one per PR. + concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('dep-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('dep-develop-{0}', github.sha) || format('dep-{0}', github.ref) }} + cancel-in-progress: true + steps: + - uses: actions/checkout@v3 + - name: Dependency analysis + run: ./gradlew buildHealth $CI_GRADLE_ARG_PROPERTIES + - name: Upload dependency analysis + if: always() + uses: actions/upload-artifact@v3 + with: + name: dependency-analysis + path: build/reports/dependency-analysis/build-health-report.txt + # Lint for main module android-lint: name: Android Linter diff --git a/CHANGES.md b/CHANGES.md index c8677b1ae4..b0203c39e9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,67 @@ +Changes in Element v1.4.18 (2022-05-31) +======================================= + +Features ✨ +---------- + - Space explore screen changes: removed space card, added rooms filtering ([#5658](https://github.com/vector-im/element-android/issues/5658)) + - Adds space or user id as a subtitle under rooms in search ([#5860](https://github.com/vector-im/element-android/issues/5860)) + - Adds up navigation in spaces ([#6073](https://github.com/vector-im/element-android/issues/6073)) + - Labs flag for enabling live location sharing ([#6098](https://github.com/vector-im/element-android/issues/6098)) + - Added support for mandatory backup or passphrase from .well-known configuration. ([#6133](https://github.com/vector-im/element-android/issues/6133)) + - Security - Asking for user confirmation when tapping URLs which contain unicode directional overrides ([#6163](https://github.com/vector-im/element-android/issues/6163)) + - Add settings switch to allow autoplaying animated images ([#6166](https://github.com/vector-im/element-android/issues/6166)) + - Live Location Sharing - User List Bottom Sheet ([#6170](https://github.com/vector-im/element-android/issues/6170)) + +Bugfixes 🐛 +---------- + - Fix some notifications not clearing when read ([#4862](https://github.com/vector-im/element-android/issues/4862)) + - Do not switch away from home space on notification when "Show all Rooms in Home" is selected. ([#5827](https://github.com/vector-im/element-android/issues/5827)) + - Use fixed text size in read receipt counter ([#5856](https://github.com/vector-im/element-android/issues/5856)) + - Revert: Use member name instead of room name in DM creation item ([#6032](https://github.com/vector-im/element-android/issues/6032)) + - Poll refactoring with unit tests ([#6074](https://github.com/vector-im/element-android/issues/6074)) + - Correct .well-known/matrix/client handling for server_names which include ports. ([#6095](https://github.com/vector-im/element-android/issues/6095)) + - Glide - Use current drawable while loading new static map image ([#6103](https://github.com/vector-im/element-android/issues/6103)) + - Fix sending multiple invites to a room reaching only one or two people ([#6109](https://github.com/vector-im/element-android/issues/6109)) + - Prevent widget web view from reloading on screen / orientation change ([#6140](https://github.com/vector-im/element-android/issues/6140)) + - Fix decrypting redacted event from sending errors ([#6148](https://github.com/vector-im/element-android/issues/6148)) + - Make widget web view request system permissions for camera and microphone (PSF-1061) ([#6149](https://github.com/vector-im/element-android/issues/6149)) + +In development 🚧 +---------------- + - Adds email input and verification screens to the new FTUE onboarding flow ([#5278](https://github.com/vector-im/element-android/issues/5278)) + - FTUE - Adds the redesigned Sign In screen ([#5283](https://github.com/vector-im/element-android/issues/5283)) + - [Live location sharing] Update message in timeline during the live ([#5689](https://github.com/vector-im/element-android/issues/5689)) + - FTUE - Overrides sign up flow ordering for matrix.org only ([#5783](https://github.com/vector-im/element-android/issues/5783)) + - Live location sharing: navigation from timeline to map screen + Live location sharing: show user pins on map screen ([#6012](https://github.com/vector-im/element-android/issues/6012)) + - FTUE - Adds homeserver login/register deeplink support ([#6023](https://github.com/vector-im/element-android/issues/6023)) + - [Live location sharing] Update entity in DB when a live is timed out ([#6123](https://github.com/vector-im/element-android/issues/6123)) + +SDK API changes ⚠️ +------------------ +- Notifies other devices when a verification request sent from an Android device is accepted.` ([#5724](https://github.com/vector-im/element-android/issues/5724)) +- Some `val` have been changed to `fun` to increase their visibility in the generated documentation. Just add `()` if you were using them. +- `KeysBackupService.state` has been replaced by `KeysBackupService.getState()` +- `KeysBackupService.isStucked` has been replaced by `KeysBackupService.isStuck()` +- SDK documentation improved ([#5952](https://github.com/vector-im/element-android/issues/5952)) +- Improve replay attacks and reduce duplicate message index errors ([#6077](https://github.com/vector-im/element-android/issues/6077)) +- Remove `RoomSummaryQueryParams.roomId`. If you need to observe a single room, use the new API `RoomService.getRoomSummaryLive(roomId: String)` +- `ActiveSpaceFilter` has been renamed to `SpaceFilter` +- `RoomCategoryFilter.ALL` has been removed, just pass `null` to not filter on Room category. ([#6143](https://github.com/vector-im/element-android/issues/6143)) + +Other changes +------------- + - leaving space experience changed to be aligned with iOS ([#5728](https://github.com/vector-im/element-android/issues/5728)) + - @Ignore a number of tests that are currently failing in CI. ([#6025](https://github.com/vector-im/element-android/issues/6025)) + - Remove ShortcutBadger lib and usage (it was dead code) ([#6041](https://github.com/vector-im/element-android/issues/6041)) + - Test: Ensure calling 'fail()' is not caught by the catch block ([#6089](https://github.com/vector-im/element-android/issues/6089)) + - Excludes transitive optional non FOSS google location dependency from fdroid builds ([#6100](https://github.com/vector-im/element-android/issues/6100)) + - Fixed grammar errors in /vector/src/main/res/values/strings.xml ([#6132](https://github.com/vector-im/element-android/issues/6132)) + - Downgrade gradle from 7.2.0 to 7.1.3 ([#6141](https://github.com/vector-im/element-android/issues/6141)) + - Add Lao language to the in-app settings. ([#6196](https://github.com/vector-im/element-android/issues/6196)) + - Remove the background location permission request ([#6198](https://github.com/vector-im/element-android/issues/6198)) + + Changes in Element v1.4.16 (2022-05-17) ======================================= @@ -36,7 +100,7 @@ Other changes - Reformatted project code ([#5953](https://github.com/vector-im/element-android/issues/5953)) - Update check for server-side threads support to match spec. ([#5997](https://github.com/vector-im/element-android/issues/5997)) - Setup detekt ([#6038](https://github.com/vector-im/element-android/issues/6038)) - - Notify the user for each new message ([#46312](https://github.com/vector-im/element-android/issues/46312)) + - Notify the user for each new message ([#4632](https://github.com/vector-im/element-android/issues/4632)) Changes in Element v1.4.14 (2022-05-05) diff --git a/build.gradle b/build.gradle index 8553428b49..635db83e14 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ buildscript { classpath 'com.google.gms:google-services:4.3.10' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' - classpath "com.likethesalad.android:stem-plugin:2.0.0" + classpath "com.likethesalad.android:stem-plugin:2.1.1" classpath 'org.owasp:dependency-check-gradle:7.1.0.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" @@ -41,6 +41,9 @@ plugins { id "org.jlleitschuh.gradle.ktlint" version "10.3.0" // Detekt id "io.gitlab.arturbosch.detekt" version "1.20.0" + + // Dependency Analysis + id 'com.autonomousapps.dependency-analysis' version "1.4.0" } // https://github.com/jeremylong/DependencyCheck @@ -219,3 +222,61 @@ project(":library:diff-match-patch") { // } // } //} + +dependencyAnalysis { + dependencies { + bundle("kotlin-stdlib") { + includeGroup("org.jetbrains.kotlin") + } + bundle("react") { + includeGroup("com.facebook.react") + } + } + issues { + all { + ignoreKtx(true) + onUsedTransitiveDependencies { + // Transitively used dependencies that should be declared directly + severity("ignore") + } + onUnusedDependencies { + severity("fail") + } + onUnusedAnnotationProcessors { + severity("fail") + exclude("com.airbnb.android:epoxy-processor", "com.google.dagger:hilt-compiler") // False positives + } + } + project(":library:jsonviewer") { + onUnusedDependencies { + exclude("org.json:json") // Used in unit tests, overwrites the one bundled into Android + } + } + project(":library:ui-styles") { + onUnusedDependencies { + exclude("com.github.vector-im:PFLockScreen-Android") // False positive + } + } + project(":matrix-sdk-android") { + onUnusedDependencies { + exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx + } + } + project(":matrix-sdk-android-flow") { + onUnusedDependencies { + exclude("androidx.paging:paging-runtime-ktx") // False positive + } + } + project(":vector") { + onUnusedDependencies { + // False positives + exclude( + "com.vanniktech:emoji-google", + "com.vanniktech:emoji-material", + "org.maplibre.gl:android-plugin-annotation-v9", + "org.maplibre.gl:android-sdk", + ) + } + } + } +} diff --git a/dependencies.gradle b/dependencies.gradle index 10f9539e5a..35ab909cf4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,10 +7,13 @@ ext.versions = [ 'targetCompat' : JavaVersion.VERSION_11, ] -def gradle = "7.2.0" + +// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142 +// Please test carefully before upgrading again. +def gradle = "7.1.3" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.6.21" -def kotlinCoroutines = "1.6.1" +def kotlinCoroutines = "1.6.2" def dagger = "2.42" def retrofit = "2.9.0" def arrow = "0.8.2" @@ -23,7 +26,7 @@ def mavericks = "2.6.1" def glide = "4.13.2" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" -def vanniktechEmoji = "0.9.0" +def vanniktechEmoji = "0.15.0" // Testing def mockk = "1.12.4" @@ -45,12 +48,13 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ + 'activity' : "androidx.activity:activity:1.4.0", 'appCompat' : "androidx.appcompat:appcompat:1.4.1", 'core' : "androidx.core:core-ktx:1.7.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", 'work' : "androidx.work:work-runtime-ktx:2.7.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", @@ -69,7 +73,9 @@ ext.libs = [ 'testRules' : "androidx.test:rules:$androidxTest", 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", - 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" + 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso", + 'viewpager2' : "androidx.viewpager2:viewpager2:1.0.0", + 'transition' : "androidx.transition:transition:1.2.0", ], google : [ 'material' : "com.google.android.material:material:1.6.0" @@ -81,7 +87,7 @@ ext.libs = [ 'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger" ], squareup : [ - 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi", + 'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" @@ -107,6 +113,10 @@ ext.libs = [ 'mavericks' : "com.airbnb.android:mavericks:$mavericks", 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" ], + maplibre : [ + 'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2", + 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0" + ], mockk : [ 'mockk' : "io.mockk:mockk:$mockk", 'mockkAndroid' : "io.mockk:mockk-android:$mockk" diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 76869fccf1..59cefe7e89 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -141,7 +141,6 @@ ext.groups = [ 'jline', 'jp.wasabeef', 'junit', - 'me.leolin', 'me.saket', 'net.bytebuddy', 'net.java', diff --git a/docs/add_threePids.md b/docs/add_threePids.md index 6fb0aff426..8d02702ca6 100644 --- a/docs/add_threePids.md +++ b/docs/add_threePids.md @@ -1,5 +1,34 @@ # Adding and removing ThreePids to an account + + +* [Add email](#add-email) + * [User enter the email](#user-enter-the-email) + * [The email is already added to an account](#the-email-is-already-added-to-an-account) + * [The email is free](#the-email-is-free) +* [User receives an e-mail](#user-receives-an-e-mail) + * [User clicks on the link](#user-clicks-on-the-link) + * [User returns on Element](#user-returns-on-element) + * [User enters his password](#user-enters-his-password) + * [The link has not been clicked](#the-link-has-not-been-clicked) + * [Wrong password](#wrong-password) + * [The link has been clicked and the account password is correct](#the-link-has-been-clicked-and-the-account-password-is-correct) +* [Remove email](#remove-email) + * [User want to remove an email from his account](#user-want-to-remove-an-email-from-his-account) + * [Email was not bound to an identity server](#email-was-not-bound-to-an-identity-server) + * [Email was bound to an identity server](#email-was-bound-to-an-identity-server) +* [Add phone number](#add-phone-number) + * [The phone number is already added to an account](#the-phone-number-is-already-added-to-an-account) + * [The phone number is free](#the-phone-number-is-free) +* [User receive a text message](#user-receive-a-text-message) + * [User enter the code to the app](#user-enter-the-code-to-the-app) + * [Wrong code](#wrong-code) + * [Correct code](#correct-code) +* [Remove phone number](#remove-phone-number) + * [User wants to remove a phone number from his account](#user-wants-to-remove-a-phone-number-from-his-account) + + + ## Add email ### User enter the email diff --git a/docs/analytics.md b/docs/analytics.md index 135ace81b0..9b2c176912 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -1,5 +1,13 @@ # Analytics in Element + + +* [Solution](#solution) +* [How to add a new Event](#how-to-add-a-new-event) +* [Forks of Element](#forks-of-element) + + + ## Solution Element is using PostHog to send analytics event. diff --git a/docs/color_migration_guide.md b/docs/color_migration_guide.md index 31a531d124..f83a88e2b2 100644 --- a/docs/color_migration_guide.md +++ b/docs/color_migration_guide.md @@ -1,5 +1,14 @@ # Color migration + + + * [Changes](#changes) + * [Main change for developers](#main-change-for-developers) + * [Remaining work](#remaining-work) + * [Migration guide](#migration-guide) + + + ### Changes - use colors defined in https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 diff --git a/docs/design.md b/docs/design.md index a79f19cf3e..f390725560 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,5 +1,31 @@ # Element Android design + + +* [Introduction](#introduction) +* [How to import from Figma to the Element Android project](#how-to-import-from-figma-to-the-element-android-project) + * [Colors](#colors) + * [Text](#text) + * [Dimension, position and margin](#dimension-position-and-margin) + * [Icons](#icons) + * [Export drawable from Figma](#export-drawable-from-figma) + * [Import in Android Studio](#import-in-android-studio) + * [Images](#images) +* [Figma links](#figma-links) + * [Coumpound](#coumpound) + * [Login](#login) + * [Login v2](#login-v2) + * [Room list](#room-list) + * [Timeline](#timeline) + * [Voice message](#voice-message) + * [Room settings](#room-settings) + * [VoIP](#voip) + * [Presence](#presence) + * [Spaces](#spaces) + * [List to be continued...](#list-to-be-continued) + + + ## Introduction Design at element.io is done using Figma - https://www.figma.com diff --git a/docs/identity_server.md b/docs/identity_server.md index e765ae3949..a1ee86cb19 100644 --- a/docs/identity_server.md +++ b/docs/identity_server.md @@ -1,5 +1,19 @@ # Identity server + + +* [Introduction](#introduction) +* [Implementation](#implementation) +* [Related MSCs](#related-mscs) +* [Steps and requirements](#steps-and-requirements) +* [Screens](#screens) + * [Settings](#settings) + * [Discovery screen](#discovery-screen) + * [Set identity server screen](#set-identity-server-screen) +* [Ref:](#ref:) + + + Issue: #607 PR: #1354 diff --git a/docs/integration_tests.md b/docs/integration_tests.md index e79f966d1f..b5a830e7ff 100644 --- a/docs/integration_tests.md +++ b/docs/integration_tests.md @@ -1,5 +1,18 @@ # Integration tests + + +* [Pre requirements](#pre-requirements) +* [Install and run Synapse](#install-and-run-synapse) +* [Run the test](#run-the-test) +* [Stop Synapse](#stop-synapse) +* [Troubleshoot](#troubleshoot) + * [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver) + * [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost:8080") + * [virtualenv command fails](#virtualenv-command-fails) + + + Integration tests are useful to ensure that the code works well for any use cases. They can also be used as sample on how to use the Matrix SDK. diff --git a/docs/jitsi.md b/docs/jitsi.md index 1b4e0a37b4..4dd06effdb 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -1,20 +1,32 @@ # Jitsi in Element Android + + +* [Native Jitsi SDK](#native-jitsi-sdk) + * [How to build the Jitsi Meet SDK](#how-to-build-the-jitsi-meet-sdk) + * [Jitsi version](#jitsi-version) + * [Run the build script](#run-the-build-script) + * [Link with the new generated library](#link-with-the-new-generated-library) + * [Sanity tests](#sanity-tests) + * [Export the build library](#export-the-build-library) + + + Native Jitsi support has been added to Element Android by the PR [#1914](https://github.com/vector-im/element-android/pull/1914). The description of the PR contains some documentation about the behaviour in each possible room configuration. Also, ensure to have a look on [the documentation from Element Web](https://github.com/vector-im/element-web/blob/develop/docs/jitsi.md) The official documentation about how to integrate the Jitsi SDK in an Android app is available here: https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk. -# Native Jitsi SDK +## Native Jitsi SDK The Jitsi SDK is built by ourselves with the flag LIBRE_BUILD, to be able to be integrated on the F-Droid version of Element Android. The generated maven repository is then host in the project https://github.com/vector-im/jitsi_libre_maven -## How to build the Jitsi Meet SDK +### How to build the Jitsi Meet SDK -### Jitsi version +#### Jitsi version Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. @@ -22,7 +34,7 @@ Latest tag can be found from this page: https://github.com/jitsi/jitsi-meet-rele Currently we are building the version with the tag `android-sdk-3.10.0`. -### Run the build script +#### Run the build script At the root of the Element Android, run the following script: @@ -32,7 +44,7 @@ At the root of the Element Android, run the following script: It will build the Jitsi Meet Android library and put every generated files in the folder `/tmp/jitsi` -### Link with the new generated library +#### Link with the new generated library - Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line: @@ -57,7 +69,7 @@ implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar' - Perform a gradle sync and build the project - Perform test -### Sanity tests +#### Sanity tests In order to validate that the upgrade of the Jitsi and WebRTC dependency does not break anything, the following sanity tests have to be performed, using two devices: - Make 1-1 audio call (so using WebRTC) @@ -65,7 +77,7 @@ In order to validate that the upgrade of the Jitsi and WebRTC dependency does no - Create and join a conference call with audio only (so using Jitsi library). Leave the conference. Join it again. - Create and join a conference call with audio and video (so using Jitsi library) Leave the conference. Join it again. -### Export the build library +#### Export the build library If all the tests are passed, you can export the generated Jitsi library to our Maven repository. @@ -81,4 +93,4 @@ url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10. - Build the project and perform the sanity tests again. -- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android. \ No newline at end of file +- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android. diff --git a/docs/notifications.md b/docs/notifications.md index b44984785a..612b8785b8 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,37 +1,42 @@ This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app. # Table of Contents -1. [Prerequisites Knowledge](#prerequisites-knowledge) - * [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver) - * [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification) - * [Push VS Notification](#push-vs-notification) - * [Push in the matrix federated world](#push-in-the-matrix-federated-world) - * [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client) - * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation) - * [Background processing limitations](#background-processing-limitations) -2. [Element Notification implementations](#element-notification-implementations) - * [Requirements](#requirements) - * [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid) - * [Push (FCM) received in background](#push-fcm-received-in-background) - * [FCM Fallback mode](#fcm-fallback-mode) - * [F-Droid background Mode](#f-droid-background-mode) -3. [Application Settings](#application-settings) + + + +* [Prerequisites Knowledge](#prerequisites-knowledge) + * [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?) + * [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification) + * [Push VS Notification](#push-vs-notification) + * [Push in the matrix federated world](#push-in-the-matrix-federated-world) + * [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?) + * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation) + * [Background processing limitations](#background-processing-limitations) +* [Element Notification implementations](#element-notification-implementations) + * [Requirements](#requirements) + * [Foreground sync mode (Gplay and F-Droid)](#foreground-sync-mode-gplay-and-f-droid) + * [Push (FCM) received in background](#push-fcm-received-in-background) + * [FCM Fallback mode](#fcm-fallback-mode) + * [F-Droid background Mode](#f-droid-background-mode) +* [Application Settings](#application-settings) + + First let's start with some prerequisite knowledge -# Prerequisites Knowledge +## Prerequisites Knowledge -## How does a matrix client get a message from a homeserver? +### How does a matrix client get a message from a homeserver? In order to get messages from a homeserver, a matrix client need to perform a ``sync`` operation. `To read events, the intended flow of operation is for clients to first call the /sync API without a since parameter. This returns the most recent message events for each room, as well as the state of the room at the start of the returned timeline. ` -The client need to call the `sync`API periodically in order to get incremental updates of the server state (new messages). +The client need to call the `sync` API periodically in order to get incremental updates of the server state (new messages). This mechanism is known as **HTTP long Polling**. -Using the **HTTP Long Polling** mechanism a client polls a server requesting new information. +Using the **HTTP Long Polling** mechanism a client polls a server requesting new information. The server *holds the request open until new data is available*. Once available, the server responds and sends the new information. When the client receives the new information, it immediately sends another request, and the operation is repeated. @@ -52,7 +57,7 @@ By default, this is 0, so the server will return immediately even if the respons When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0. -## How does a mobile app receives push notification +### How does a mobile app receives push notification Push notification is used as a way to wake up a mobile application when some important information is available and should be processed. @@ -66,22 +71,22 @@ FCM will only work on android devices that have Google plays services installed (In simple terms, Google Play Services is a background service that runs on Android, which in turn helps in integrating Google’s advanced functionalities to other applications) De-Googlified devices need to rely on something else in order to stay up to date with a server. -There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls- , - privacy and or independency requirement, source code licence) +There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls-, + privacy and or independence requirement, source code licence) -## Push VS Notification +### Push VS Notification This need some disambiguation, because it is the source of common confusion: -*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH plateform.* +*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH platform.* Technically there is a difference between a push and a notification. A notification is what you see on screen and/or in the notification Menu/Drawer (in the top bar of the phone). Notifications are not always triggered by a push (One can display a notification locally triggered by an alarm) -## Push in the matrix federated world +### Push in the matrix federated world In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication! This server is called a **Push Gateway** in the matrix world @@ -118,11 +123,11 @@ Client/Server API + | | | | | ``` Recommended reading: - * https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html +* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html * https://matrix.org/docs/spec/client_server/r0.4.0.html#id128 -## How does the homeserver know when to notify a client? +### How does the homeserver know when to notify a client? This is defined by [**push rules**](https://matrix.org/docs/spec/client_server/r0.4.0.html#push-rules-). @@ -140,14 +145,14 @@ Of course, content patterns matching cannot be used for encrypted messages serve That is why clients are able to **process the push rules client side** to decide what kind of notification should be presented for a given event. -## Push vs privacy, and mitigation +### Push vs privacy, and mitigation As seen previously, App developers don't directly send a push to the end user's device, they use a Push Provider as intermediary. So technically this intermediary is able to read the content of what is sent. App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to it's server in order to generate a local notification. -## Background processing limitations +### Background processing limitations A mobile applications process live in a managed word, meaning that its process can be limited (e.g no network access), stopped or killed at almost anytime by the Operating System. @@ -167,15 +172,15 @@ The documentation on this subject is vague, and as per our experiments not alway It is getting more and more complex to have reliable notifications when FCM is not used. -# Element Notification implementations +## Element Notification implementations -## Requirements +### Requirements Element Android must work with and without FCM. * The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present) * The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services) -## Foreground sync mode (Gplay & F-Droid) +### Foreground sync mode (Gplay and F-Droid) When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling). @@ -183,9 +188,9 @@ As this mode does not need to live beyond the scope of the application, and as p This mode is turned on when the app enters foreground, and off when enters background. -In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service) +In background, and depending on whether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service) -## Push (FCM) received in background +### Push (FCM) received in background In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the homeserver. @@ -225,10 +230,10 @@ Upon reception of the FCM push, Element will perform a sync call to the homeserv Element implements several strategies in these cases (TODO document) -## FCM Fallback mode +### FCM Fallback mode It is possible that Element is not able to get a FCM push token. -Common errors (amoung several others) that can cause that: +Common errors (among several others) that can cause that: * Google Play Services is outdated * Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`) @@ -246,7 +251,7 @@ Usually in this mode, what happen is when you take back your phone in your hand, The fallback mode is supposed to be a temporary state waiting for the user to fix issues for FCM, or for App Developers that has done a fork to correctly configure their FCM settings. -## F-Droid background Mode +### F-Droid background Mode The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push. @@ -256,7 +261,7 @@ Only solution left is to use `AlarmManager`, that offers new API to allow launch Notice that these alarms, due to their potential impact on battery life, can still be restricted by the system. Documentation says that they will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn. -These restrictions can be relaxed by requirering the app to be white listed from battery optimization. +These restrictions can be relaxed by requiring the app to be white listed from battery optimization. F-Droid version will schedule alarms that will then trigger a Broadcast Receiver, that in turn will launch a Service (in the classic android way), and the reschedule an alarm for next time. @@ -266,9 +271,7 @@ That is why on Element F-Droid, the broadcast receiver will acquire a temporary Note that foreground services require to put a notification informing the user that the app is doing something even if not launched). - - -# Application Settings +## Application Settings **Notifications > Enable notifications for this account** diff --git a/docs/pull_request.md b/docs/pull_request.md index 94f8f3b6d8..d2d2bb7a3b 100644 --- a/docs/pull_request.md +++ b/docs/pull_request.md @@ -1,5 +1,43 @@ # Pull requests + + +* [Introduction](#introduction) +* [Who should read this document?](#who-should-read-this-document?) +* [Submitting PR](#submitting-pr) + * [Who can submit pull requests?](#who-can-submit-pull-requests?) + * [Humans](#humans) + * [Draft PR?](#draft-pr?) + * [Base branch](#base-branch) + * [PR Review Assignment](#pr-review-assignment) + * [PR review time](#pr-review-time) + * [Re-request PR review](#re-request-pr-review) + * [When create split PR?](#when-create-split-pr?) + * [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr) + * [Bots](#bots) + * [Dependabot](#dependabot) + * [Gradle wrapper](#gradle-wrapper) + * [Sync analytics plan](#sync-analytics-plan) +* [Reviewing PR](#reviewing-pr) + * [Who can review pull requests?](#who-can-review-pull-requests?) + * [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr) + * [Rules](#rules) + * [Check the form](#check-the-form) + * [PR title](#pr-title) + * [PR description](#pr-description) + * [File change](#file-change) + * [Check the commit](#check-the-commit) + * [Check the substance](#check-the-substance) + * [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr) + * [What happen to the issue(s)?](#what-happen-to-the-issues?) + * [Merge conflict](#merge-conflict) + * [When and who can merge PR](#when-and-who-can-merge-pr) + * [Merge type](#merge-type) + * [Resolve conversation](#resolve-conversation) +* [Responsibility](#responsibility) + + + ## Introduction This document gives some clue about how to efficiently manage Pull Requests (PR). This document is a first draft and may be improved later. diff --git a/docs/signin.md b/docs/signin.md index 0a234d2a20..65e305389f 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -2,6 +2,27 @@ This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver. + + +* [Sign in flows](#sign-in-flows) + * [Get the flow](#get-the-flow) + * [Login with username](#login-with-username) + * [Incorrect password](#incorrect-password) + * [Correct password:](#correct-password:) + * [Login with email](#login-with-email) + * [Unknown email](#unknown-email) + * [Known email, wrong password](#known-email-wrong-password) + * [Known email, correct password](#known-email-correct-password) + * [Login with Msisdn](#login-with-msisdn) + * [Login with SSO](#login-with-sso) +* [Reset password](#reset-password) + * [Send email](#send-email) + * [When the email is not known](#when-the-email-is-not-known) + * [When the email is known](#when-the-email-is-known) + * [User clicks on the link](#user-clicks-on-the-link) + + + ## Sign in flows ### Get the flow @@ -322,4 +343,4 @@ curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds": {} ``` -The password has been changed, and all the existing token are invalidated. User can now login with the new password. \ No newline at end of file +The password has been changed, and all the existing token are invalidated. User can now login with the new password. diff --git a/docs/signup.md b/docs/signup.md index 97cd20a423..0b01338965 100644 --- a/docs/signup.md +++ b/docs/signup.md @@ -4,6 +4,20 @@ This document describes the flow of registration to a homeserver. Examples come *Ref*: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + + +* [Sign up flows](#sign-up-flows) + * [First step](#first-step) + * [Step 1: entering user name and password](#step-1:-entering-user-name-and-password) + * [If username already exists](#if-username-already-exists) + * [Step 2: entering email](#step-2:-entering-email) + * [Step 2 bis: user enters an email](#step-2-bis:-user-enters-an-email) + * [Step 3: Accepting T&C](#step-3:-accepting-t&c) + * [Step 4: Captcha](#step-4:-captcha) + * [Step 5: MSISDN](#step-5:-msisdn) + + + ## Sign up flows ### First step diff --git a/docs/ui-tests.md b/docs/ui-tests.md index 667a6ed7fb..d0b9db6818 100644 --- a/docs/ui-tests.md +++ b/docs/ui-tests.md @@ -10,6 +10,20 @@ Currently the test are covering a small set of application flows: - Self verification via emoji - Self verification via passphrase + + +* [Prerequisites:](#prerequisites:) +* [Run the tests](#run-the-tests) + * [From the source code](#from-the-source-code) + * [From command line](#from-command-line) +* [Recipes](#recipes) + * [Wait for initial sync](#wait-for-initial-sync) + * [Accessing current activity](#accessing-current-activity) + * [Interact with other session](#interact-with-other-session) + * [Contributing to the UiAllScreensSanityTest](#contributing-to-the-uiallscreenssanitytest) + + + ## Prerequisites: Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses). diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt new file mode 100644 index 0000000000..d369efa9f2 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Zlepšení správy ignorovaných uživatelů. Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40104180.txt b/fastlane/metadata/android/en-US/changelogs/40104180.txt new file mode 100644 index 0000000000..61db61727a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104180.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/es-ES/changelogs/40104140.txt b/fastlane/metadata/android/es-ES/changelogs/40104140.txt new file mode 100644 index 0000000000..595cd6b3d4 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Cambios principales en esta versión: Mejoras en la administración de usuarios ignorados. Varias correciones de bugs y mejoras en la estabilidad. +Registro de cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104140.txt b/fastlane/metadata/android/et/changelogs/40104140.txt new file mode 100644 index 0000000000..9000616f38 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: eiratud kasutajate parem haldus ning erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104140.txt b/fastlane/metadata/android/fa/changelogs/40104140.txt new file mode 100644 index 0000000000..cf5d7bc6ac --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104140.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: مدیریت بهبودیافتهٔ کاربران چشم‌پوشیده. رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104100.txt b/fastlane/metadata/android/fr-FR/changelogs/40104100.txt new file mode 100644 index 0000000000..f2453c5539 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104100.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Défilement dans les messages vocaux. 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/40104110.txt b/fastlane/metadata/android/fr-FR/changelogs/40104110.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104110.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/40104120.txt b/fastlane/metadata/android/fr-FR/changelogs/40104120.txt new file mode 100644 index 0000000000..accd82fe72 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104120.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Les utilisateurs peuvent apparaître hors-ligne. Ajout d’un lecteur pour les pièces jointes audio +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104130.txt b/fastlane/metadata/android/fr-FR/changelogs/40104130.txt new file mode 100644 index 0000000000..accd82fe72 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104130.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Les utilisateurs peuvent apparaître hors-ligne. Ajout d’un lecteur pour les pièces jointes audio +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104140.txt b/fastlane/metadata/android/fr-FR/changelogs/40104140.txt new file mode 100644 index 0000000000..087d5bc1c8 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Amélioration de la gestion des utilisateurs ignorés. 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/id/changelogs/40104140.txt b/fastlane/metadata/android/id/changelogs/40104140.txt new file mode 100644 index 0000000000..174e8dcded --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Tingkatkan pengelolaan pengguna yang diabaikan. Beberapa perbaikan kutu dan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104140.txt b/fastlane/metadata/android/it-IT/changelogs/40104140.txt new file mode 100644 index 0000000000..ae367e72d0 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: migliorata la gestione degli utenti ignorati. Varie correzioni e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/lo-LA/changelogs/40104140.txt b/fastlane/metadata/android/lo-LA/changelogs/40104140.txt new file mode 100644 index 0000000000..e8c8d84031 --- /dev/null +++ b/fastlane/metadata/android/lo-LA/changelogs/40104140.txt @@ -0,0 +1,2 @@ +ການປ່ຽນແປງຫຼັກໃນສະບັບນີ້: ປັບປຸງການບໍລິຫານການລະເວັ້ນຜູ້ໃຊ້. ປັບປຸງບັກ ແລະຄວາມສະຖຽນ. +ບັນທຶກການປ່ຽນແປງສະບັບເຕັມ: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101120.txt b/fastlane/metadata/android/pl-PL/changelogs/40101120.txt new file mode 100644 index 0000000000..6a62f1c6a9 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: aktualizacja motywu i stylu oraz naprawa awarii po rozmowie wideo +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101130.txt b/fastlane/metadata/android/pl-PL/changelogs/40101130.txt new file mode 100644 index 0000000000..ec8d488eb8 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: głównie aktualizacja stabilności i poprawki błędów. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101140.txt b/fastlane/metadata/android/pl-PL/changelogs/40101140.txt new file mode 100644 index 0000000000..c4c102da4a --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: naprawienie problemu z zaszyfrowanymi wiadomościami. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101150.txt b/fastlane/metadata/android/pl-PL/changelogs/40101150.txt new file mode 100644 index 0000000000..2eb1a3f018 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: implementacja wiadomości głosowych w ustawieniach laboratorium. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40101160.txt b/fastlane/metadata/android/pl-PL/changelogs/40101160.txt new file mode 100644 index 0000000000..682da8be76 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Naprawiono błąd podczas wysyłania zaszyfrowanej wiadomości, jeśli ktoś w pokoju się wyloguje. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40102000.txt b/fastlane/metadata/android/pl-PL/changelogs/40102000.txt new file mode 100644 index 0000000000..cdae0a4ba7 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Wiadomość głosowa jest domyślnie włączona. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40102010.txt b/fastlane/metadata/android/pl-PL/changelogs/40102010.txt new file mode 100644 index 0000000000..0a825e8672 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Wiele ulepszeń w VoIP i Przestrzeniach (nadal w wersji beta). +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103000.txt b/fastlane/metadata/android/pl-PL/changelogs/40103000.txt new file mode 100644 index 0000000000..8b408ced72 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Organizuj swoje pokoje za pomocą Przestrzeni! +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103010.txt b/fastlane/metadata/android/pl-PL/changelogs/40103010.txt new file mode 100644 index 0000000000..0a49e7fa68 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Organizuj swoje pokoje za pomocą Przestrzeni! Wersja 1.3.1 naprawia awarię, która może wystąpić w wersji 1.3.0. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103020.txt b/fastlane/metadata/android/pl-PL/changelogs/40103020.txt new file mode 100644 index 0000000000..3e37f64b76 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Dodano obsługę Android Auto. Wiele poprawek błędów! +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103030.txt b/fastlane/metadata/android/pl-PL/changelogs/40103030.txt new file mode 100644 index 0000000000..8f80d95b5a --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Uwidocznij politykę(-i) serwera tożsamości w ustawieniach. Tymczasowo usunięto obsługę Androida Auto. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40103040.txt b/fastlane/metadata/android/pl-PL/changelogs/40103040.txt new file mode 100644 index 0000000000..13458a7b2d --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40103040.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Dodanie obsługi obecności, dla pokoju wiadomości bezpośrednich (uwaga: obecność jest wyłączona na matrix.org). Dodano ponownie obsługę Androida Auto. +Pełna lista zmian: https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/pl-PL/changelogs/40104140.txt b/fastlane/metadata/android/pl-PL/changelogs/40104140.txt new file mode 100644 index 0000000000..84ab57a2ac --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Główne zmiany w tej wersji: Poprawa zarządzania ignorowanymi użytkownikami. Różne poprawki błędów i ulepszenia stabilności. +Pełna lista zmian: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40104140.txt b/fastlane/metadata/android/pt-BR/changelogs/40104140.txt new file mode 100644 index 0000000000..ed1a53c910 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Melhorar gerenciamento de usuárias(os) ignoradas(os). Vários consertos de bugs e melhorias de estabilidade. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104140.txt b/fastlane/metadata/android/sk/changelogs/40104140.txt new file mode 100644 index 0000000000..e1c85961a4 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Zlepšenie správy ignorovaných používateľov. 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/40104140.txt b/fastlane/metadata/android/sv-SE/changelogs/40104140.txt new file mode 100644 index 0000000000..9b58878dfb --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Förbättra hantering av ignorerade användare. Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ta-IN/changelogs/40104140.txt b/fastlane/metadata/android/ta-IN/changelogs/40104140.txt new file mode 100644 index 0000000000..f3196b57b6 --- /dev/null +++ b/fastlane/metadata/android/ta-IN/changelogs/40104140.txt @@ -0,0 +1,2 @@ +இந்த பதிப்பில் உள்ள முதன்மை மாற்றங்கள்: தவிர்க்கப்பட்ட பயனர்களின் மேலாண்மை மேம்படுத்தப்பட்டுள்ளது. வெவ்வேறு வழுக்களைச் சரிசெய்தல் மற்றும் நிலைப்புத்தன்மையை மேம்படுத்தல். +முழு மாற்ற அறிக்கை: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ta-IN/full_description.txt b/fastlane/metadata/android/ta-IN/full_description.txt new file mode 100644 index 0000000000..9aa693bda2 --- /dev/null +++ b/fastlane/metadata/android/ta-IN/full_description.txt @@ -0,0 +1,42 @@ +Element is both a secure messenger and a productivity team collaboration app that is ideal for group chats while remote working. This chat app uses end-to-end encryption to provide powerful video conferencing, file sharing and voice calls. + +Element இன் தனிச்சிறப்புகளுள் சில: +- மேம்பட்ட இயங்கலை தொடர்பு கருவிகள் +- தொலைநிலையில் உள்ள ஊழியர்களுக்கும், பாதுகாப்பான நிறும கருத்து பரிமாற்றங்களை அனுமதிப்பதற்காக, முழுவதுமாக மறைகுறியாக்கப்பட்ட செய்திகள் +- MATRIX திறந்த மூல கட்டமைப்பை அடிப்படையாக கொண்டு செயல்படும் அதிகாரப்பரவலாக்கப்பட்ட அரட்டை +- செயல்திட்டங்களை மேலாண்மை செய்யும் போது, மறைகுறியாக்கப்பட்ட தரவுடன் கூடிய பாதுகாப்பான கோப்பு பகிரல் +- IP மூலம் குரல் (VoIP) மற்றும் திரை பகிரல் உடன் கூடிய குரல் அரட்டைகள் +- உங்கள் மனம் கவர்ந்த இயங்கலை உடனிணைவு கருவிகள், செயல்திட்ட மேலாண்மை கருவிகள், VoIP சேவைகள் மற்றும் இதர குழு தூதுரை செயலிகள் உடன் கூடிய எளிமையான ஒருமைப்பாடு + +Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages. + +தனியுரிமை மற்றும் மறைகுறியாக்கப்பட்ட செய்தி அனுப்பல் +தேவையில்லாத விளம்பரங்கள், தரவு சுரண்டல் மற்றும் தகவல் கட்டுப்பாடு போன்றவற்றில் இருந்து Element உங்களை பாதுகாக்கிறது. மேலும், இது முனைக்கு-முனை மறைகுறியாக்கம் மற்றும் குறுக்கு-ஒப்பமிடப்பட்ட சாதன சரிபார்ப்பு ஆகியவற்றின் மூலம் உங்கள் எல்லா தரவுகள், ஒன்றுக்கொன்றான காணொளி மற்றும் குரல் அழைப்புகளை பாதுகாக்கிறது. + +Element gives you control over your privacy while allowing you to communicate securely with யாரோனும் ஒருவருடன் on the Matrix network, or other business collaboration tools by integrating with apps such as Slack. + +Element can be self-hosted +To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility. + +உங்கள் தரவைச் சொந்தமாக்கிக் கொள்ளுங்கள் +தரவுகள் மற்றும் செய்திகளை எங்கு சேமித்து வைக்க வேண்டும் என்பதை நீங்கள் முடிவு செய்கிறீர்கள். இதன்மூலம், தரவு சுரண்டல் மற்றும் மூன்றாம் தரப்பினர் அனுகல் ஆகிய இடர்களை தவிர்க்கலாம். + +Element வெவ்வேறு வகையில் கட்டுப்பாட்டை உங்களிடம் அளிக்கிறது: +1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers +2. Self-host your account by running a server on your own IT infrastructure +3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform + +திறந்த செய்தி அனுப்பல் மற்றும் ஒருமைப்பாடு +You can chat with anyone on the Matrix network, whether they’re using Element, another Matrix app or even if they are using a different messaging app. + +மிகவும் பாதுகாப்பானது +உண்மையான முனைக்கு-முனை மறைகுறியாக்கம் (உரையாடலில் உள்ளவர்கள் மட்டுமே மறைகுறியாக்கத்தை நீக்கி செய்தியை காண இயலும்) மற்றும் குறுக்கு-ஒப்பமிடப்பட்ட சாதன சரிபார்ப்பு. + +முழுமையான தொடர்பு மற்றும் ஒருமைப்பாடு +செய்தி அனுப்பல், காணொளி மற்றும் குரல் அழைப்புகளை, கோப்பு பகிரல், திரை பகிரல் மற்றும் ஒருமைப்பாடுகள், இயலிகள் மற்றும் நிரல் பலகைகளின் மொத்த கொத்து. அறைகள், குழுக்களை உருவாக்கி, அவர்களுடன் உரையாடி, வேலையை எளிமையாக்கவும். + +எங்கு விட்டு சென்றீர்களோ அதிலிருந்த துவங்கவும் +Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io + +திறந்த மூலம் +Element Android ஒரு திறந்த மூல செயல் திட்டமாகும். இது GitHub இல் தொகுத்து வழங்கப்பட்டுள்ளது. வழுக்கள் ஏதேனும் கண்டறிந்தால் மற்றும்/அல்லது இதன் வளர்ச்சிக்கு பங்களிக்க விரும்பினால், https://github.com/vector-im/element-android என்னும் தளத்திற்கு வருகை தரவும். diff --git a/fastlane/metadata/android/ta-IN/short_description.txt b/fastlane/metadata/android/ta-IN/short_description.txt new file mode 100644 index 0000000000..9c7afb2a37 --- /dev/null +++ b/fastlane/metadata/android/ta-IN/short_description.txt @@ -0,0 +1 @@ +மறைகுறியாக்கப்பட்ட செய்தி அனுப்பல், குழு அரட்டை மற்றும் காணொளி அழைப்புகள் diff --git a/fastlane/metadata/android/ta-IN/title.txt b/fastlane/metadata/android/ta-IN/title.txt new file mode 100644 index 0000000000..ecb9a01c06 --- /dev/null +++ b/fastlane/metadata/android/ta-IN/title.txt @@ -0,0 +1 @@ +Element - பாதுகாப்பான தூதுரை சேவை diff --git a/fastlane/metadata/android/uk/changelogs/40104140.txt b/fastlane/metadata/android/uk/changelogs/40104140.txt new file mode 100644 index 0000000000..293ad117e4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Удосконалено керування нехтуваними користувачами. Різні виправлення помилок та поліпшення стабільності. +Вичерпний журнал змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104140.txt b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt new file mode 100644 index 0000000000..ff830dab7c --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104140.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:改善被忽略使用者的管理。多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/library/attachment-viewer/build.gradle b/library/attachment-viewer/build.gradle index 048710f62c..8bbafd3387 100644 --- a/library/attachment-viewer/build.gradle +++ b/library/attachment-viewer/build.gradle @@ -55,5 +55,6 @@ dependencies { implementation libs.androidx.appCompat implementation libs.androidx.recyclerview - implementation libs.google.material -} \ No newline at end of file + api libs.androidx.viewpager2 + implementation libs.androidx.transition +} diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle index d3afd8d29b..0f7789a2a8 100644 --- a/library/core-utils/build.gradle +++ b/library/core-utils/build.gradle @@ -50,6 +50,5 @@ android { } dependencies { - implementation libs.androidx.appCompat implementation libs.jetbrains.coroutinesAndroid } diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt index 2efb439ace..aeb5ae7914 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt @@ -16,6 +16,7 @@ package im.vector.lib.core.utils.flow +import android.os.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -68,10 +69,10 @@ fun Flow.chunk(durationInMillis: Long): Flow> { @ExperimentalCoroutinesApi fun Flow.throttleFirst(windowDuration: Long): Flow = flow { - var windowStartTime = System.currentTimeMillis() + var windowStartTime = SystemClock.elapsedRealtime() var emitted = false collect { value -> - val currentTime = System.currentTimeMillis() + val currentTime = SystemClock.elapsedRealtime() val delta = currentTime - windowStartTime if (delta >= windowDuration) { windowStartTime += delta / windowDuration * windowDuration diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index 2110747feb..e1a3b0c9ee 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation libs.androidx.appCompat implementation libs.androidx.core + implementation libs.androidx.recyclerview implementation libs.airbnb.epoxy kapt libs.airbnb.epoxyProcessor @@ -60,7 +61,6 @@ dependencies { // Span utils implementation 'me.gujun.android:span:1.7' - implementation libs.google.material implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid 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 fac7099b37..66dfcc5dc3 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt @@ -18,11 +18,11 @@ package org.billcarsonfr.jsonviewer import android.content.ClipData import android.content.ClipboardManager -import android.content.Context import android.view.ContextMenu import android.view.View import android.widget.LinearLayout import android.widget.TextView +import androidx.core.content.getSystemService import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyHolder import com.airbnb.epoxy.EpoxyModelClass @@ -77,8 +77,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder() { ) { if (copyValue != null) { val menuItem = menu?.add(R.string.copy_value) - val clipService = - v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + val clipService = v?.context?.getSystemService() menuItem?.setOnMenuItemClickListener { clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue)) true diff --git a/library/multipicker/build.gradle b/library/multipicker/build.gradle index bb98a2f852..2de99d5c20 100644 --- a/library/multipicker/build.gradle +++ b/library/multipicker/build.gradle @@ -38,9 +38,9 @@ android { } dependencies { - implementation libs.androidx.appCompat - implementation libs.androidx.fragmentKtx + api libs.androidx.activity implementation libs.androidx.exifinterface + implementation libs.androidx.core // Log implementation libs.jakewharton.timber diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 0ac513b252..31cfdd24c7 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -60,4 +60,4 @@ dependencies { implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen implementation 'im.dlg:android-dialer:1.2.5' -} \ No newline at end of file +} diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml b/library/ui-styles/src/main/res/drawable/bg_color_background.xml similarity index 100% rename from library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml rename to library/ui-styles/src/main/res/drawable/bg_color_background.xml diff --git a/library/ui-styles/src/main/res/drawable/bg_pin_key.xml b/library/ui-styles/src/main/res/drawable/bg_pin_key.xml index d4a54577be..5bf293aab0 100644 --- a/library/ui-styles/src/main/res/drawable/bg_pin_key.xml +++ b/library/ui-styles/src/main/res/drawable/bg_pin_key.xml @@ -10,4 +10,4 @@ android:height="70dp" /> - \ No newline at end of file + 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_waiting_for_email_verification.xml new file mode 100644 index 0000000000..cdd4c20a4d --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml b/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml index 1827a7682b..879cac15ca 100644 --- a/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml +++ b/library/ui-styles/src/main/res/drawable/pin_code_dot_empty.xml @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml b/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml index 799ea30174..83bdac5126 100644 --- a/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml +++ b/library/ui-styles/src/main/res/drawable/pin_code_dot_fill.xml @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/drawable/pin_code_dots.xml b/library/ui-styles/src/main/res/drawable/pin_code_dots.xml index 29e445e511..c4b1073f85 100644 --- a/library/ui-styles/src/main/res/drawable/pin_code_dots.xml +++ b/library/ui-styles/src/main/res/drawable/pin_code_dots.xml @@ -6,4 +6,4 @@ android:drawable="@drawable/pin_code_dot_fill"/> - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/values/styles_location.xml b/library/ui-styles/src/main/res/values/styles_location.xml index 5563d28342..9d9fc862f6 100644 --- a/library/ui-styles/src/main/res/values/styles_location.xml +++ b/library/ui-styles/src/main/res/values/styles_location.xml @@ -2,10 +2,42 @@ + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/styles_pin_code.xml b/library/ui-styles/src/main/res/values/styles_pin_code.xml index 2b6c113359..cb22863694 100644 --- a/library/ui-styles/src/main/res/values/styles_pin_code.xml +++ b/library/ui-styles/src/main/res/values/styles_pin_code.xml @@ -41,4 +41,4 @@ ?vctr_content_primary - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml index c86eeb8efb..20c375c2d6 100644 --- a/library/ui-styles/src/main/res/values/styles_timeline.xml +++ b/library/ui-styles/src/main/res/values/styles_timeline.xml @@ -1,5 +1,5 @@ - + + - \ No newline at end of file + diff --git a/matrix-sdk-android-flow/build.gradle b/matrix-sdk-android-flow/build.gradle index ea43ce20c8..fb69af2d82 100644 --- a/matrix-sdk-android-flow/build.gradle +++ b/matrix-sdk-android-flow/build.gradle @@ -31,9 +31,7 @@ android { } dependencies { - implementation project(":matrix-sdk-android") - implementation libs.androidx.appCompat implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -41,7 +39,4 @@ dependencies { // Paging implementation libs.androidx.pagingRuntimeKtx - - // Logging - implementation libs.jakewharton.timber } diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 9f260858f6..2839e6ab61 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -45,6 +45,13 @@ import org.matrix.android.sdk.api.util.toOptional class FlowSession(private val session: Session) { + fun liveRoomSummary(roomId: String): Flow> { + return session.roomService().getRoomSummaryLive(roomId).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.roomService().getRoomSummary(roomId).toOptional() + } + } + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE): Flow> { return session.roomService().getRoomSummariesLive(queryParams, sortOrder).asFlow() .startWith(session.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index dfa99d0f8a..52ee019e7b 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -56,7 +56,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.16\"" + buildConfigField "String", "SDK_VERSION", "\"1.4.18\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -136,7 +136,6 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - implementation libs.androidx.appCompat implementation libs.androidx.core // Lifecycle @@ -155,12 +154,11 @@ dependencies { implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' - implementation 'com.squareup.okhttp3:okhttp-urlconnection' implementation libs.squareup.moshi kapt libs.squareup.moshiKotlin - implementation libs.markwon.core + api "com.atlassian.commonmark:commonmark:0.13.0" // Image implementation libs.androidx.exifinterface @@ -176,10 +174,6 @@ dependencies { // Work implementation libs.androidx.work - // FP - implementation libs.arrow.core - implementation libs.arrow.instances - // olm lib is now hosted in MavenCentral implementation 'org.matrix.android:olm-sdk:3.2.11' @@ -198,11 +192,9 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49' testImplementation libs.tests.junit - testImplementation 'org.robolectric:robolectric:4.7.3' - //testImplementation 'org.robolectric:shadows-support-v4:3.0' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 testImplementation libs.mockk.mockk testImplementation libs.tests.kluent diff --git a/matrix-sdk-android/docs/packages.md b/matrix-sdk-android/docs/packages.md index ae7bee1b4e..19f7c15a7a 100644 --- a/matrix-sdk-android/docs/packages.md +++ b/matrix-sdk-android/docs/packages.md @@ -1,3 +1,7 @@ +# Package org.matrix.android.sdk.userstories + +This package contains some user stories (**Us** prefix) of the SDK usage. You will find example of what it is possible to do with the SDK and the API which can be used to do it. + # Package org.matrix.android.sdk.api This is the root package of the API exposed by this SDK. diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/Util.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/Util.kt new file mode 100644 index 0000000000..5e2c2ba25f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/Util.kt @@ -0,0 +1,44 @@ +/* + * 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 + +import junit.framework.TestCase.fail + +/** + * Will fail the test if invoking [block] is not throwing a Throwable. + * + * @param message the failure message, if the block does not throw any Throwable + * @param failureBlock a Lambda to be able to do extra check on the thrown Throwable + * @param block the block to test + */ +internal suspend fun mustFail( + message: String = "must fail", + failureBlock: ((Throwable) -> Unit)? = null, + block: suspend () -> Unit, +) { + val isSuccess = try { + block.invoke() + true + } catch (throwable: Throwable) { + failureBlock?.invoke(throwable) + false + } + + if (isSuccess) { + fail(message) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index 486bc02769..7e4fc4768f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -24,8 +24,8 @@ 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.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants @@ -34,32 +34,22 @@ import org.matrix.android.sdk.common.TestConstants @LargeTest class AccountCreationTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - @Test - fun createAccountTest() { - val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) - - commonTestHelper.signOutAndClose(session) + fun createAccountTest() = runSessionTest(context()) { commonTestHelper -> + commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) } @Test @Ignore("This test will be ignored until it is fixed") - fun createAccountAndLoginAgainTest() { + fun createAccountAndLoginAgainTest() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Log again to the same account - val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) - - commonTestHelper.signOutAndClose(session) - commonTestHelper.signOutAndClose(session2) + commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) } @Test - fun simpleE2eTest() { - val res = cryptoTestHelper.doE2ETestWithAliceInARoom() - - res.cleanUp(commonTestHelper) + fun simpleE2eTest() = runCryptoTest(context()) { cryptoTestHelper, _ -> + cryptoTestHelper.doE2ETestWithAliceInARoom() } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index 6d740c5a34..260e8dbe05 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -25,7 +25,7 @@ import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.failure.isInvalidPassword -import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants @@ -34,14 +34,12 @@ import org.matrix.android.sdk.common.TestConstants @Ignore("This test will be ignored until it is fixed") class ChangePasswordTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - companion object { private const val NEW_PASSWORD = "this is a new password" } @Test - fun changePasswordTest() { + fun changePasswordTest() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) // Change password @@ -54,9 +52,6 @@ class ChangePasswordTest : InstrumentedTest { throwable.isInvalidPassword().shouldBeTrue() // Try to login with the new password, should work - val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false)) - - commonTestHelper.signOutAndClose(session) - commonTestHelper.signOutAndClose(session2) + commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false)) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index d2dfe4d945..0b21f85742 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -29,7 +29,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError -import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import kotlin.coroutines.Continuation @@ -39,10 +39,8 @@ import kotlin.coroutines.resume @FixMethodOrder(MethodSorters.JVM) class DeactivateAccountTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test - fun deactivateAccountTest() { + fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper -> val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Deactivate the account diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt index 9371154aaf..8dbff82015 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/network/ApiInterceptorTest.kt @@ -23,7 +23,7 @@ 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.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import timber.log.Timber @@ -32,10 +32,8 @@ import timber.log.Timber @FixMethodOrder(MethodSorters.JVM) class ApiInterceptorTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test - fun apiInterceptorTest() { + fun apiInterceptorTest() = runSessionTest(context()) { commonTestHelper -> val responses = mutableListOf() val listener = object : ApiInterceptorListener { 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 e33e4faea2..fdfaceeb4d 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 @@ -54,12 +54,39 @@ import java.util.concurrent.TimeUnit * This class exposes methods to be used in common cases * Registration, login, Sync, Sending messages... */ -class CommonTestHelper(context: Context) { +class CommonTestHelper private constructor(context: Context) { + + companion object { + internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context) + return try { + block(testHelper) + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } + } + } + + internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context) + val cryptoTestHelper = CryptoTestHelper(testHelper) + return try { + block(cryptoTestHelper, testHelper) + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } + } + } + } internal val matrix: TestMatrix private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var accountNumber = 0 + private val trackedSessions = mutableListOf() + fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor init { @@ -84,6 +111,15 @@ class CommonTestHelper(context: Context) { return logIntoAccount(userId, TestConstants.PASSWORD, testParams) } + fun cleanUpOpenedSessions() { + trackedSessions.forEach { + runBlockingTest { + it.signOutService().signOut(true) + } + } + trackedSessions.clear() + } + /** * Create a homeserver configuration, with Http connection allowed for test */ @@ -96,7 +132,7 @@ class CommonTestHelper(context: Context) { /** * This methods init the event stream and check for initial sync * - * @param session the session to sync + * @param session the session to sync */ fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { val lock = CountDownLatch(1) @@ -119,7 +155,7 @@ class CommonTestHelper(context: Context) { /** * This methods clear the cache and waits for initialSync * - * @param session the session to sync + * @param session the session to sync */ fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { waitWithLatch(timeout) { latch -> @@ -142,8 +178,8 @@ class CommonTestHelper(context: Context) { /** * Sends text messages in a room * - * @param room the room where to send the messages - * @param message the message to send + * @param room the room where to send the messages + * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { @@ -207,8 +243,8 @@ class CommonTestHelper(context: Context) { /** * Reply in a thread - * @param room the room where to send the messages - * @param message the message to send + * @param room the room where to send the messages + * @param message the message to send * @param numberOfMessages the number of time the message will be sent */ fun replyInThreadMessage( @@ -232,8 +268,8 @@ class CommonTestHelper(context: Context) { * Creates a unique account * * @param userNamePrefix the user name prefix - * @param password the password - * @param testParams test params about the session + * @param password the password + * @param testParams test params about the session * @return the session associated with the newly created account */ private fun createAccount(userNamePrefix: String, @@ -245,14 +281,16 @@ class CommonTestHelper(context: Context) { testParams ) assertNotNull(session) - return session + return session.also { + trackedSessions.add(session) + } } /** * Logs into an existing account * - * @param userId the userId to log in - * @param password the password to log in + * @param userId the userId to log in + * @param password the password to log in * @param testParams test params about the session * @return the session associated with the existing account */ @@ -261,14 +299,16 @@ class CommonTestHelper(context: Context) { testParams: SessionTestParams): Session { val session = logAccountAndSync(userId, password, testParams) assertNotNull(session) - return session + return session.also { + trackedSessions.add(session) + } } /** * Create an account and a dedicated session * - * @param userName the account username - * @param password the password + * @param userName the account username + * @param password the password * @param sessionTestParams parameters for the test */ private fun createAccountAndSync(userName: String, @@ -297,7 +337,7 @@ class CommonTestHelper(context: Context) { val session = (registrationResult as RegistrationResult.Success).session session.open() if (sessionTestParams.withInitialSync) { - syncSession(session, 60_000) + syncSession(session, 120_000) } return session } @@ -305,8 +345,8 @@ class CommonTestHelper(context: Context) { /** * Start an account login * - * @param userName the account username - * @param password the password + * @param userName the account username + * @param password the password * @param sessionTestParams session test params */ private fun logAccountAndSync(userName: String, @@ -378,7 +418,10 @@ class CommonTestHelper(context: Context) { * @throws InterruptedException */ fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { - assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + assertTrue( + "Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.", + latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + ) } suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { @@ -433,6 +476,7 @@ class CommonTestHelper(context: Context) { fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } fun signOutAndClose(session: Session) { + trackedSessions.remove(session) runBlockingTest(timeout = 60_000) { session.signOutService().signOut(true) } 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 348841313b..5fd86d4fdb 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 @@ -58,7 +58,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner -import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.toBase64NoPadding @@ -66,7 +66,7 @@ import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume -class CryptoTestHelper(private val testHelper: CommonTestHelper) { +class CryptoTestHelper(val testHelper: CommonTestHelper) { private val messagesFromAlice: List = listOf("0 - Hello I'm Alice!", "4 - Go!") private val messagesFromBob: List = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.") @@ -361,19 +361,19 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { ssssService.storeSecret( MASTER_KEY_SSSS_NAME, session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) ssssService.storeSecret( USER_SIGNING_KEY_SSSS_NAME, session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) // set up megolm backup @@ -390,7 +390,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { ssssService.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, secret, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt index b16ab98e6c..39f49a9ccc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt @@ -40,6 +40,9 @@ class RetryTestRule(val retryCount: Int = 3) : TestRule { for (i in 0 until retryCount) { try { base.evaluate() + if (i > 0) { + println("Retried test $i times") + } return } catch (t: Throwable) { caughtThrowable = t diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt index 0f79896b2c..89c965c31a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt @@ -23,7 +23,7 @@ object TestConstants { const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" // Time out to use when waiting for server response. - private const val AWAIT_TIME_OUT_MILLIS = 60_000 + private const val AWAIT_TIME_OUT_MILLIS = 120_000 // Time out to use when waiting for server response, when the debugger is connected. 10 minutes private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt index e663cc1865..5864a801e6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt @@ -21,7 +21,8 @@ import android.os.Handler import android.os.Looper import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration -import androidx.work.WorkManager +import androidx.work.impl.WorkManagerImpl +import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration @@ -66,7 +67,12 @@ internal class TestMatrix(context: Context, matrixConfiguration: MatrixConfigura .setExecutor(Executors.newCachedThreadPool()) .setWorkerFactory(matrixWorkerFactory) .build() - WorkManager.initialize(appContext, configuration) + val delegate = WorkManagerImpl( + context, + configuration, + WorkManagerTaskExecutor(configuration.taskExecutor) + ) + WorkManagerImpl.setDelegate(delegate) uiHandler.post { ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt index e823aa39a1..cd6c146f03 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -22,9 +22,11 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.util.time.DefaultClock @@ -37,6 +39,8 @@ private const val DUMMY_DEVICE_KEY = "DeviceKey" @RunWith(AndroidJUnit4::class) class CryptoStoreTest : InstrumentedTest { + @get:Rule val rule = RetryTestRule(3) + private val cryptoStoreHelper = CryptoStoreHelper() private val clock = DefaultClock() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt new file mode 100644 index 0000000000..a48b45a1f5 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class DecryptRedactedEventTest : InstrumentedTest { + + @Test + fun doNotFailToDecryptRedactedEvent() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val e2eRoomID = testData.roomId + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + val roomALicePOV = aliceSession.getRoom(e2eRoomID)!! + val timelineEvent = testHelper.sendTextMessage(roomALicePOV, "Hello", 1).first() + val redactionReason = "Wrong Room" + roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) + + // get the event from bob + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true + } + } + + val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! + + testHelper.runBlockingTest { + try { + val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") + Assert.assertEquals( + "Unexpected redacted reason", + redactionReason, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason") + ) + Assert.assertEquals( + "Unexpected Redacted event id", + timelineEvent.eventId, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts + ) + } catch (failure: Throwable) { + Assert.fail("Should not throw when decrypting a redacted event") + } + } + } +} 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 ebe4c5ff6f..010383dab8 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,6 +23,8 @@ 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 import org.junit.runners.JUnit4 @@ -56,17 +58,23 @@ 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.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest +import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.mustFail import java.util.concurrent.CountDownLatch @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) + /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -77,9 +85,7 @@ class E2eeSanityTests : InstrumentedTest { * Alice sends a new message, then check that the new one can be decrypted */ @Test - fun testSendingE2EEMessages() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testSendingE2EEMessages() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession @@ -193,21 +199,12 @@ class E2eeSanityTests : InstrumentedTest { } } } - - otherAccounts.forEach { - testHelper.signOutAndClose(it) - } - newAccount.forEach { testHelper.signOutAndClose(it) } - - cryptoTestData.cleanUp(testHelper) } @Test - fun testKeyGossipingIsEnabledByDefault() { - val testHelper = CommonTestHelper(context()) + fun testKeyGossipingIsEnabledByDefault() = runSessionTest(context()) { testHelper -> val session = testHelper.createAccount("alice", SessionTestParams(true)) Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled()) - testHelper.signOutAndClose(session) } /** @@ -225,9 +222,7 @@ class E2eeSanityTests : InstrumentedTest { * 9. Check that new session can decrypt */ @Test - fun testBasicBackupImport() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testBasicBackupImport() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession @@ -339,8 +334,6 @@ class E2eeSanityTests : InstrumentedTest { // ensure bob can now decrypt cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) - - testHelper.signOutAndClose(newBobSession) } /** @@ -348,9 +341,7 @@ class E2eeSanityTests : InstrumentedTest { * get them from an older one. */ @Test - fun testSimpleGossip() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testSimpleGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession @@ -444,18 +435,13 @@ class E2eeSanityTests : InstrumentedTest { } cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) - - cryptoTestData.cleanUp(testHelper) - testHelper.signOutAndClose(newBobSession) } /** * Test that if a better key is forwarded (lower index, it is then used) */ @Test - fun testForwardBetterKey() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession @@ -521,10 +507,8 @@ class E2eeSanityTests : InstrumentedTest { // Confirm we can decrypt one but not the other testHelper.runBlockingTest { - try { + mustFail(message = "Should not be able to decrypt event") { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - fail("Should not be able to decrypt event") - } catch (_: MXCryptoError) { } } @@ -573,10 +557,6 @@ class E2eeSanityTests : InstrumentedTest { canDecryptFirst && canDecryptSecond } } - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSessionWithBetterKey) - testHelper.signOutAndClose(newBobSession) } private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { @@ -607,9 +587,7 @@ class E2eeSanityTests : InstrumentedTest { * Test that if a better key is forwared (lower index, it is then used) */ @Test - fun testSelfInteractiveVerificationAndGossip() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) cryptoTestHelper.bootstrapSecurity(aliceSession) @@ -748,9 +726,6 @@ class E2eeSanityTests : InstrumentedTest { aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version ) - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(aliceNewSession) } private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { 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 93aa78a305..e37ae5be86 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 @@ -30,18 +30,14 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) class PreShareKeysTest : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - @Test - fun ensure_outbound_session_happy_path() { + fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val e2eRoomID = testData.roomId val aliceSession = testData.firstSession @@ -94,7 +90,5 @@ class PreShareKeysTest : InstrumentedTest { bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE } } - - testData.cleanUp(testHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 0f3a4b4181..5fe7376184 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm @@ -63,8 +62,6 @@ import kotlin.coroutines.resume class UnwedgingTest : InstrumentedTest { private lateinit var messagesReceivedByBob: List - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) @Before fun init() { @@ -85,7 +82,7 @@ class UnwedgingTest : InstrumentedTest { * -> This is automatically fixed after SDKs restarted the olm session */ @Test - fun testUnwedging() { + fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -240,8 +237,6 @@ class UnwedgingTest : InstrumentedTest { } bobTimeline.dispose() - - cryptoTestData.cleanUp(testHelper) } private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index a37626dc20..05790bfb7d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -25,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -38,8 +37,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import kotlin.coroutines.Continuation @@ -50,11 +49,8 @@ import kotlin.coroutines.resume @LargeTest class XSigningTest : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - @Test - fun test_InitializeAndStoreKeys() { + fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) testHelper.doSync { @@ -88,7 +84,7 @@ class XSigningTest : InstrumentedTest { } @Test - fun test_CrossSigningCheckBobSeesTheKeys() { + fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -138,13 +134,10 @@ class XSigningTest : InstrumentedTest { ) assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) - - cryptoTestData.cleanUp(testHelper) } @Test - @Ignore("This test will be ignored until it is fixed") - fun test_CrossSigningTestAliceTrustBobNewDevice() { + fun test_CrossSigningTestAliceTrustBobNewDevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -218,9 +211,5 @@ class XSigningTest : InstrumentedTest { val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) - testHelper.signOutAndClose(bobSession2) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index 85b6c21df3..5f26fda946 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestHelper import java.util.concurrent.CountDownLatch @@ -42,35 +43,36 @@ import java.util.concurrent.CountDownLatch @FixMethodOrder(MethodSorters.NAME_ASCENDING) class EncryptionTest : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - @Test fun test_EncryptionEvent() { - performTest(roomShouldBeEncrypted = false) { room -> - // Send an encryption Event as an Event (and not as a state event) - room.sendService().sendEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } - } - - @Test - fun test_EncryptionStateEvent() { - performTest(roomShouldBeEncrypted = true) { room -> - runBlocking { - // Send an encryption Event as a State Event - room.stateService().sendStateEvent( + runCryptoTest(context()) { cryptoTestHelper, testHelper -> + performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room -> + // Send an encryption Event as an Event (and not as a state event) + room.sendService().sendEvent( eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = "", - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() ) } } } - private fun performTest(roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) { + @Test + fun test_EncryptionStateEvent() { + runCryptoTest(context()) { cryptoTestHelper, testHelper -> + performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room -> + runBlocking { + // Send an encryption Event as a State Event + room.stateService().sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) + } + } + } + } + + private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false) val aliceSession = cryptoTestData.firstSession @@ -109,6 +111,5 @@ class EncryptionTest : InstrumentedTest { room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted it.countDown() } - cryptoTestData.cleanUp(testHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 2e4fd62822..b16e4b82eb 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -21,11 +21,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue -import junit.framework.TestCase.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.Assert.assertNull import org.junit.FixMethodOrder +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -41,20 +41,21 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.mustFail @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class KeyShareTests : InstrumentedTest { + @get:Rule val rule = RetryTestRule(3) + @Test - fun test_DoNotSelfShareIfNotTrusted() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") @@ -91,12 +92,10 @@ class KeyShareTests : InstrumentedTest { assertNotNull(receivedEvent) assert(receivedEvent!!.isEncrypted()) - try { - commonTestHelper.runBlockingTest { + commonTestHelper.runBlockingTest { + mustFail { aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } - fail("should fail") - } catch (failure: Throwable) { } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -164,12 +163,10 @@ class KeyShareTests : InstrumentedTest { } } - try { - commonTestHelper.runBlockingTest { + commonTestHelper.runBlockingTest { + mustFail { aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } - fail("should fail") - } catch (failure: Throwable) { } // Mark the device as trusted @@ -194,9 +191,7 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareIfWasIntendedToBeShared() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -227,9 +222,7 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareToUnverifiedIfWasIntendedToBeShared() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) val aliceSession = testData.firstSession @@ -266,9 +259,7 @@ class KeyShareTests : InstrumentedTest { * Tests that keys reshared with own verified session are done from the earliest known index */ @Test - fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -388,10 +379,7 @@ class KeyShareTests : InstrumentedTest { * Tests that we don't cancel a request to early on first forward if the index is not good enough */ @Test - fun test_dontCancelToEarly() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - + fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -442,7 +430,7 @@ class KeyShareTests : InstrumentedTest { // Should get a reply from bob and not from alice commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { - // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } val result = bobReply?.result diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index cb31a2232f..0aac4297e4 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import org.junit.Assert import org.junit.FixMethodOrder +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -35,21 +36,22 @@ import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.MockOkHttpInterceptor +import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.mustFail @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class WithHeldTests : InstrumentedTest { + @get:Rule val rule = RetryTestRule(3) + @Test - fun test_WithHeldUnverifiedReason() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_WithHeldUnverifiedReason() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> // ============================= // ARRANGE @@ -92,17 +94,19 @@ class WithHeldTests : InstrumentedTest { // ============================= // Bob should not be able to decrypt because the keys is withheld - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + ) { bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } // Let's see if the reply we got from bob first session is unverified @@ -133,28 +137,23 @@ class WithHeldTests : InstrumentedTest { } // Previous message should still be undecryptable (partially withheld session) - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + }) { bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) - testHelper.signOutAndClose(bobUnverifiedSession) } @Test - fun test_WithHeldNoOlm() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -186,17 +185,18 @@ class WithHeldTests : InstrumentedTest { // Previous message should still be undecryptable (partially withheld session) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + }) { bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) } // Ensure that alice has marked the session to be shared with bob @@ -230,14 +230,10 @@ class WithHeldTests : InstrumentedTest { Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) aliceInterceptor.clearRules() - testData.cleanUp(testHelper) - testHelper.signOutAndClose(bobSecondSession) } @Test - fun test_WithHeldKeyRequest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -284,8 +280,5 @@ class WithHeldTests : InstrumentedTest { wc?.code == WithHeldCode.UNAUTHORISED } } - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSecondSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 9136272b1e..c6e17e8c44 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 @@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -43,8 +44,9 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation 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.session.getRoom -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest +import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestMatrixCallback import java.util.Collections @@ -55,15 +57,15 @@ import java.util.concurrent.CountDownLatch @LargeTest class KeysBackupTest : InstrumentedTest { + @get:Rule val rule = RetryTestRule(3) + /** * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys * - Check backup keys after having marked one as backed up * - Reset keys backup markers */ @Test - fun roomKeysTest_testBackupStore_ok() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -102,8 +104,7 @@ class KeysBackupTest : InstrumentedTest { * Check that prepareKeysBackupVersionWithPassword returns valid data */ @Test - fun prepareKeysBackupVersionTest() { - val testHelper = CommonTestHelper(context()) + fun prepareKeysBackupVersionTest() = runSessionTest(context()) { testHelper -> val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) @@ -113,7 +114,7 @@ class KeysBackupTest : InstrumentedTest { val stateObserver = StateObserver(keysBackup) - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) val megolmBackupCreationInfo = testHelper.doSync { keysBackup.prepareKeysBackupVersion(null, null, it) @@ -125,16 +126,13 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(megolmBackupCreationInfo.recoveryKey) stateObserver.stopAndCheckStates(null) - testHelper.signOutAndClose(bobSession) } /** * Test creating a keys backup version and check that createKeysBackupVersion() returns valid data */ @Test - fun createKeysBackupVersionTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) cryptoTestHelper.initializeCrossSigning(bobSession) @@ -143,13 +141,13 @@ class KeysBackupTest : InstrumentedTest { val stateObserver = StateObserver(keysBackup) - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) val megolmBackupCreationInfo = testHelper.doSync { keysBackup.prepareKeysBackupVersion(null, null, it) } - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) // Create the version val version = testHelper.doSync { @@ -157,7 +155,7 @@ class KeysBackupTest : InstrumentedTest { } // Backup must be enable now - assertTrue(keysBackup.isEnabled) + assertTrue(keysBackup.isEnabled()) // Check that it's signed with MSK val versionResult = testHelper.doSync { @@ -193,7 +191,6 @@ class KeysBackupTest : InstrumentedTest { } stateObserver.stopAndCheckStates(null) - testHelper.signOutAndClose(bobSession) } /** @@ -201,9 +198,7 @@ class KeysBackupTest : InstrumentedTest { * - Check the backup completes */ @Test - fun backupAfterCreateKeysBackupVersionTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -238,16 +233,13 @@ class KeysBackupTest : InstrumentedTest { KeysBackupState.ReadyToBackUp ) ) - cryptoTestData.cleanUp(testHelper) } /** * Check that backupAllGroupSessions() returns valid data */ @Test - fun backupAllGroupSessionsTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -281,7 +273,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) stateObserver.stopAndCheckStates(null) - cryptoTestData.cleanUp(testHelper) } /** @@ -293,9 +284,7 @@ class KeysBackupTest : InstrumentedTest { * - Compare the decrypted megolm key with the original one */ @Test - fun testEncryptAndDecryptKeysBackupData() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -330,7 +319,6 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) stateObserver.stopAndCheckStates(null) - cryptoTestData.cleanUp(testHelper) } /** @@ -340,9 +328,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test - fun restoreKeysBackupTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -428,9 +414,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test - fun trustKeyBackupVersionTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Do an e2e backup to the homeserver with a recovery key @@ -441,8 +425,8 @@ class KeysBackupTest : InstrumentedTest { // - The new device must see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device testHelper.doSync { @@ -458,7 +442,7 @@ class KeysBackupTest : InstrumentedTest { // - Backup must be enabled on the new device, on the same version assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) - assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server val keysVersionResult = testHelper.doSync { @@ -477,7 +461,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cleanUp(testHelper) } /** @@ -491,9 +474,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test - fun trustKeyBackupVersionWithRecoveryKeyTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Do an e2e backup to the homeserver with a recovery key @@ -504,8 +485,8 @@ class KeysBackupTest : InstrumentedTest { // - The new device must see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the recovery key testHelper.doSync { @@ -521,7 +502,7 @@ class KeysBackupTest : InstrumentedTest { // - Backup must be enabled on the new device, on the same version assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) - assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server val keysVersionResult = testHelper.doSync { @@ -540,7 +521,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cleanUp(testHelper) } /** @@ -552,9 +532,7 @@ class KeysBackupTest : InstrumentedTest { * - The backup must still be untrusted and disabled */ @Test - fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Do an e2e backup to the homeserver with a recovery key @@ -565,8 +543,8 @@ class KeysBackupTest : InstrumentedTest { // - The new device must see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong recovery key val latch = CountDownLatch(1) @@ -579,11 +557,10 @@ class KeysBackupTest : InstrumentedTest { // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) stateObserver.stopAndCheckStates(null) - testData.cleanUp(testHelper) } /** @@ -597,9 +574,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test - fun trustKeyBackupVersionWithPasswordTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "Password" @@ -612,8 +587,8 @@ class KeysBackupTest : InstrumentedTest { // - The new device must see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the password testHelper.doSync { @@ -629,7 +604,7 @@ class KeysBackupTest : InstrumentedTest { // - Backup must be enabled on the new device, on the same version assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) - assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server val keysVersionResult = testHelper.doSync { @@ -648,7 +623,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals(2, keysBackupVersionTrust.signatures.size) stateObserver.stopAndCheckStates(null) - testData.cleanUp(testHelper) } /** @@ -660,9 +634,7 @@ class KeysBackupTest : InstrumentedTest { * - The backup must still be untrusted and disabled */ @Test - fun trustKeyBackupVersionWithWrongPasswordTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "Password" @@ -676,8 +648,8 @@ class KeysBackupTest : InstrumentedTest { // - The new device must see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong password val latch = CountDownLatch(1) @@ -690,11 +662,10 @@ class KeysBackupTest : InstrumentedTest { // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) - assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) - assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) stateObserver.stopAndCheckStates(null) - testData.cleanUp(testHelper) } /** @@ -704,9 +675,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test - fun restoreKeysBackupWithAWrongRecoveryKeyTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -730,8 +699,6 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - - testData.cleanUp(testHelper) } /** @@ -741,9 +708,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test - fun testBackupWithPassword() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "password" @@ -790,8 +755,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - - testData.cleanUp(testHelper) } /** @@ -801,9 +764,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test - fun restoreKeysBackupWithAWrongPasswordTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "password" @@ -830,8 +791,6 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - - testData.cleanUp(testHelper) } /** @@ -841,9 +800,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test - fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "password" @@ -863,8 +820,6 @@ class KeysBackupTest : InstrumentedTest { } keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - - testData.cleanUp(testHelper) } /** @@ -874,9 +829,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test - fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -900,8 +853,6 @@ class KeysBackupTest : InstrumentedTest { // onSuccess may not have been called assertNull(importRoomKeysResult) - - testData.cleanUp(testHelper) } /** @@ -909,9 +860,7 @@ class KeysBackupTest : InstrumentedTest { * - Check the returned KeysVersionResult is trusted */ @Test - fun testIsKeysBackupTrusted() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Create a backup version @@ -945,7 +894,6 @@ class KeysBackupTest : InstrumentedTest { assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) - cryptoTestData.cleanUp(testHelper) } /** @@ -957,9 +905,7 @@ class KeysBackupTest : InstrumentedTest { * -> That must fail and her backup state must be WrongBackUpVersion */ @Test - fun testBackupWhenAnotherBackupWasCreated() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Create a backup version @@ -969,7 +915,7 @@ class KeysBackupTest : InstrumentedTest { val stateObserver = StateObserver(keysBackup) - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) // Wait for keys backup to be finished val latch0 = CountDownLatch(1) @@ -993,7 +939,7 @@ class KeysBackupTest : InstrumentedTest { // - Make alice back up her keys to her homeserver keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) - assertTrue(keysBackup.isEnabled) + assertTrue(keysBackup.isEnabled()) testHelper.await(latch0) @@ -1012,11 +958,10 @@ class KeysBackupTest : InstrumentedTest { testHelper.await(latch2) // -> That must fail and her backup state must be WrongBackUpVersion - assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state) - assertFalse(keysBackup.isEnabled) + assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) + assertFalse(keysBackup.isEnabled()) stateObserver.stopAndCheckStates(null) - cryptoTestData.cleanUp(testHelper) } /** @@ -1032,9 +977,7 @@ class KeysBackupTest : InstrumentedTest { * -> It must success */ @Test - fun testBackupAfterVerifyingADevice() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Create a backup version @@ -1069,7 +1012,7 @@ class KeysBackupTest : InstrumentedTest { // - Try to backup all in aliceSession2, it must fail val keysBackup2 = aliceSession2.cryptoService().keysBackupService() - assertFalse("Backup should not be enabled", keysBackup2.isEnabled) + assertFalse("Backup should not be enabled", keysBackup2.isEnabled()) val stateObserver2 = StateObserver(keysBackup2) @@ -1088,8 +1031,8 @@ class KeysBackupTest : InstrumentedTest { assertFalse(isSuccessful) // Backup state must be NotTrusted - assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.state) - assertFalse("Backup should not be enabled", keysBackup2.isEnabled) + assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) + assertFalse("Backup should not be enabled", keysBackup2.isEnabled()) // - Validate the old device from the new one aliceSession2.cryptoService().setDeviceVerification( @@ -1103,7 +1046,7 @@ class KeysBackupTest : InstrumentedTest { keysBackup2.addListener(object : KeysBackupStateListener { override fun onStateChange(newState: KeysBackupState) { // Check the backup completes - if (keysBackup2.state == KeysBackupState.ReadyToBackUp) { + if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { // Remove itself from the list of listeners keysBackup2.removeListener(this) @@ -1121,12 +1064,10 @@ class KeysBackupTest : InstrumentedTest { } // -> It must success - assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled) + assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled()) stateObserver.stopAndCheckStates(null) stateObserver2.stopAndCheckStates(null) - testHelper.signOutAndClose(aliceSession2) - cryptoTestData.cleanUp(testHelper) } /** @@ -1134,9 +1075,7 @@ class KeysBackupTest : InstrumentedTest { * - Delete the backup */ @Test - fun deleteKeysBackupTest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Create a backup version @@ -1146,19 +1085,18 @@ class KeysBackupTest : InstrumentedTest { val stateObserver = StateObserver(keysBackup) - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) - assertTrue(keysBackup.isEnabled) + assertTrue(keysBackup.isEnabled()) // Delete the backup testHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } // Backup is now disabled - assertFalse(keysBackup.isEnabled) + assertFalse(keysBackup.isEnabled()) stateObserver.stopAndCheckStates(null) - cryptoTestData.cleanUp(testHelper) } } 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 2220536e28..4eccdfad1f 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 @@ -106,7 +106,7 @@ internal class KeysBackupTestHelper( Assert.assertNotNull(megolmBackupCreationInfo) - Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled) + Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) // Create the version val keysVersion = testHelper.doSync { @@ -116,7 +116,7 @@ internal class KeysBackupTestHelper( Assert.assertNotNull("Key backup version should not be null", keysVersion.version) // Backup must be enable now - Assert.assertTrue(keysBackup.isEnabled) + Assert.assertTrue(keysBackup.isEnabled()) stateObserver.stopAndCheckStates(null) return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version) @@ -128,7 +128,7 @@ internal class KeysBackupTestHelper( */ fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { // If already in the wanted state, return - if (session.cryptoService().keysBackupService().state == state) { + if (session.cryptoService().keysBackupService().getState() == state) { return } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt new file mode 100644 index 0000000000..53cf802b91 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -0,0 +1,109 @@ +/* + * 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.replayattack + +import androidx.test.filters.LargeTest +import org.amshove.kluent.internal.assertFailsWith +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class ReplayAttackTest : InstrumentedTest { + + @Test + fun replayAttackAlreadyDecryptedEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! + + // Bob + val bobSession = cryptoTestData.secondSession + val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!! + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + + // Alice will send a message + val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1) + assertEquals(1, sentEvents.size) + + val fakeEventId = sentEvents[0].eventId + "_fake" + val fakeEventWithTheSameIndex = + sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId)) + + testHelper.runBlockingTest { + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + // Lets decrypt the fake event that will have the same message index + val exception = assertFailsWith { + // An exception should be thrown while the same index would have been used for the previous decryption + aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) + } + assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) + } + cryptoTestData.cleanUp(testHelper) + } + + @Test + fun replayAttackSameEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! + + // Bob + val bobSession = cryptoTestData.secondSession + val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!! + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + + // Alice will send a message + val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1) + Assert.assertTrue("Message should be sent", sentEvents.size == 1) + assertEquals(sentEvents.size, 1) + + testHelper.runBlockingTest { + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + try { + // Lets try to decrypt the same event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + } catch (ex: Throwable) { + fail("Shouldn't throw a decryption error for same event") + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index c758050fc9..c8be6aae74 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -31,15 +31,16 @@ import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.KeySigner import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError -import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toBase64NoPadding import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService @@ -55,8 +56,7 @@ class QuadSTests : InstrumentedTest { } @Test - fun test_Generate4SKey() { - val testHelper = CommonTestHelper(context()) + fun test_Generate4SKey() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -108,12 +108,11 @@ class QuadSTests : InstrumentedTest { } @Test - fun test_StoreSecret() { - val testHelper = CommonTestHelper(context()) + fun test_StoreSecret() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId = "My.Key" - val info = generatedSecret(aliceSession, keyId, true) + val info = generatedSecret(testHelper, aliceSession, keyId, true) val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) @@ -123,11 +122,11 @@ class QuadSTests : InstrumentedTest { aliceSession.sharedSecretStorageService().storeSecret( "secret.of.life", clearSecret, - listOf(SharedSecretStorageService.KeyRef(null, keySpec)) // default key + listOf(KeyRef(null, keySpec)) // default key ) } - val secretAccountData = assertAccountData(aliceSession, "secret.of.life") + val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life") val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> assertNotNull("Element should be encrypted", encryptedContent) @@ -149,12 +148,10 @@ class QuadSTests : InstrumentedTest { } assertEquals("Secret mismatch", clearSecret, decryptedSecret) - testHelper.signOutAndClose(aliceSession) } @Test - fun test_SetDefaultLocalEcho() { - val testHelper = CommonTestHelper(context()) + fun test_SetDefaultLocalEcho() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -170,19 +167,16 @@ class QuadSTests : InstrumentedTest { testHelper.runBlockingTest { quadS.setDefaultKey(TEST_KEY_ID) } - - testHelper.signOutAndClose(aliceSession) } @Test - fun test_StoreSecretWithMultipleKey() { - val testHelper = CommonTestHelper(context()) + fun test_StoreSecretWithMultipleKey() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" - val key1Info = generatedSecret(aliceSession, keyId1, true) + val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true) val keyId2 = "Key2" - val key2Info = generatedSecret(aliceSession, keyId2, true) + val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" @@ -191,8 +185,8 @@ class QuadSTests : InstrumentedTest { "my.secret", mySecretText.toByteArray().toBase64NoPadding(), listOf( - SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), - SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) ) ) } @@ -221,19 +215,16 @@ class QuadSTests : InstrumentedTest { RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! ) } - - testHelper.signOutAndClose(aliceSession) } @Test @Ignore("Test is working locally, not in GitHub actions") - fun test_GetSecretWithBadPassphrase() { - val testHelper = CommonTestHelper(context()) + fun test_GetSecretWithBadPassphrase() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val passphrase = "The good pass phrase" - val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) + val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" @@ -241,7 +232,7 @@ class QuadSTests : InstrumentedTest { aliceSession.sharedSecretStorageService().storeSecret( "my.secret", mySecretText.toByteArray().toBase64NoPadding(), - listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) + listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) ) } @@ -275,13 +266,9 @@ class QuadSTests : InstrumentedTest { ) ) } - - testHelper.signOutAndClose(aliceSession) } - private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { - val testHelper = CommonTestHelper(context()) - + private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent { var accountData: UserAccountDataEvent? = null testHelper.waitWithLatch { val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) @@ -297,29 +284,27 @@ class QuadSTests : InstrumentedTest { return accountData!! } - private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val testHelper = CommonTestHelper(context()) val creationInfo = testHelper.runBlockingTest { quadS.generateKey(keyId, null, keyId, emptyKeySigner) } - assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { testHelper.runBlockingTest { quadS.setDefaultKey(keyId) } - assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo } - private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val testHelper = CommonTestHelper(context()) val creationInfo = testHelper.runBlockingTest { quadS.generateKeyWithPassphrase( @@ -331,12 +316,12 @@ class QuadSTests : InstrumentedTest { ) } - assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { testHelper.runBlockingTest { quadS.setDefaultKey(keyId) } - assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index 2892cf8464..c4d9ba4ee3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart import org.matrix.android.sdk.internal.crypto.model.rest.toValue @@ -56,9 +55,7 @@ import java.util.concurrent.CountDownLatch class SASTest : InstrumentedTest { @Test - fun test_aliceStartThenAliceCancel() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -135,15 +132,11 @@ class SASTest : InstrumentedTest { assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) - - cryptoTestData.cleanUp(testHelper) } @Test @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_protocols_must_include_curve25519() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -195,15 +188,11 @@ class SASTest : InstrumentedTest { testHelper.await(cancelLatch) assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) - - cryptoTestData.cleanUp(testHelper) } @Test @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_macs_Must_include_hmac_sha256() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -236,15 +225,11 @@ class SASTest : InstrumentedTest { val cancelReq = canceledToDeviceEvent!!.content.toModel()!! assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - - cryptoTestData.cleanUp(testHelper) } @Test @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_short_code_include_decimal() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -277,8 +262,6 @@ class SASTest : InstrumentedTest { val cancelReq = canceledToDeviceEvent!!.content.toModel()!! assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - - cryptoTestData.cleanUp(testHelper) } private fun fakeBobStart(bobSession: Session, @@ -314,9 +297,7 @@ class SASTest : InstrumentedTest { // any two devices may only have at most one key verification in flight at a time. // If a device has two verifications in progress with the same device, then it should cancel both verifications. @Test - fun test_aliceStartTwoRequests() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -357,9 +338,7 @@ class SASTest : InstrumentedTest { */ @Test @Ignore("This test will be ignored until it is fixed") - fun test_aliceAndBobAgreement() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -413,14 +392,10 @@ class SASTest : InstrumentedTest { accepted!!.shortAuthenticationStrings.forEach { assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) } - - cryptoTestData.cleanUp(testHelper) } @Test - fun test_aliceAndBobSASCode() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -473,14 +448,10 @@ class SASTest : InstrumentedTest { "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), bobTx.getShortCodeRepresentation(SasMode.DECIMAL) ) - - cryptoTestData.cleanUp(testHelper) } @Test - fun test_happyPath() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -553,13 +524,10 @@ class SASTest : InstrumentedTest { assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) - cryptoTestData.cleanUp(testHelper) } @Test - fun test_ConcurrentStart() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -646,7 +614,5 @@ class SASTest : InstrumentedTest { bobPovTx?.state == VerificationTxState.ShortCodeReady } } - - cryptoTestData.cleanUp(testHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index df3b2ffe27..462f47cafc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -27,11 +27,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest +import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation @@ -152,9 +154,7 @@ class VerificationTest : InstrumentedTest { private fun doTest(aliceSupportedMethods: List, bobSupportedMethods: List, expectedResultForAlice: ExpectedResult, - expectedResultForBob: ExpectedResult) { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + expectedResultForBob: ExpectedResult) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -249,7 +249,48 @@ class VerificationTest : InstrumentedTest { pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode } + } - cryptoTestData.cleanUp(testHelper) + @Test + fun test_selfVerificationAcceptedCancelsItForOtherSessions() = runSessionTest(context()) { testHelper -> + val defaultSessionParams = SessionTestParams(true) + + val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) + val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) + + val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) + + val serviceOfVerified = aliceSessionToVerify.cryptoService().verificationService() + val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService() + val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService() + + serviceOfVerifier.addListener(object : VerificationService.Listener { + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // Accept verification request + serviceOfVerifier.readyPendingVerification( + verificationMethods, + pr.otherUserId, + pr.transactionId!!, + ) + } + }) + + serviceOfVerified.requestKeyVerification( + methods = verificationMethods, + otherUserId = aliceSessionToVerify.myUserId, + otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId), + ) + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) + requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } + } + } + + testHelper.signOutAndClose(aliceSessionToVerify) + testHelper.signOutAndClose(aliceSessionThatVerifies) + testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt index a2984dd27e..1ffcc2a3e6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt @@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @@ -44,9 +43,7 @@ import java.util.concurrent.CountDownLatch class ThreadMessagingTest : InstrumentedTest { @Test - fun reply_in_thread_should_create_a_thread() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun reply_in_thread_should_create_a_thread() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -104,9 +101,7 @@ class ThreadMessagingTest : InstrumentedTest { } @Test - fun reply_in_thread_should_create_a_thread_from_other_user() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun reply_in_thread_should_create_a_thread_from_other_user() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -179,9 +174,7 @@ class ThreadMessagingTest : InstrumentedTest { } @Test - fun reply_in_thread_to_timeline_message_multiple_times() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun reply_in_thread_to_timeline_message_multiple_times() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -244,9 +237,7 @@ class ThreadMessagingTest : InstrumentedTest { } @Test - fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt index 61ab6d4b40..2b72ecc52a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt @@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @@ -47,9 +46,7 @@ import java.util.concurrent.CountDownLatch class PollAggregationTest : InstrumentedTest { @Test - fun testAllPollUseCases() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -138,7 +135,6 @@ class PollAggregationTest : InstrumentedTest { aliceSession.stopSync() aliceTimeline.dispose() - cryptoTestData.cleanUp(commonTestHelper) } private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index d5b4a07fc0..3dd3f5fa2a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.checkSendOrder import timber.log.Timber import java.util.concurrent.CountDownLatch @@ -53,9 +52,7 @@ class TimelineForwardPaginationTest : InstrumentedTest { * This test ensure that if we click to permalink, we will be able to go back to the live */ @Test - fun forwardPaginationTest() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun forwardPaginationTest() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val numberOfMessagesToSend = 90 val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) @@ -140,9 +137,24 @@ class TimelineForwardPaginationTest : InstrumentedTest { aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType()) - // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination - // 6 + 1 + 50 - assertEquals(57, snapshot.size) + + // We explicitly test all the types we expect here, as we expect 51 messages and "some" state events + // But state events can change over time. So this acts as a kinda documentation of what we expect and + // provides a good error message if it doesn't match + + val snapshotTypes = mutableMapOf() + snapshot.groupingBy { it -> it.root.type }.eachCountTo(snapshotTypes) + // Some state events on room creation + assertEquals("m.room.name", 1, snapshotTypes.remove("m.room.name")) + assertEquals("m.room.guest_access", 1, snapshotTypes.remove("m.room.guest_access")) + assertEquals("m.room.history_visibility", 1, snapshotTypes.remove("m.room.history_visibility")) + assertEquals("m.room.join_rules", 1, snapshotTypes.remove("m.room.join_rules")) + assertEquals("m.room.power_levels", 1, snapshotTypes.remove("m.room.power_levels")) + assertEquals("m.room.create", 1, snapshotTypes.remove("m.room.create")) + assertEquals("m.room.member", 1, snapshotTypes.remove("m.room.member")) + // 50 from pagination + 1 context + assertEquals("m.room.message", 51, snapshotTypes.remove("m.room.message")) + assertEquals("Additional events found in timeline", setOf(), snapshotTypes.keys) } // Alice paginates once again FORWARD for 50 events @@ -152,8 +164,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { val snapshot = runBlocking { aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50) } - // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) - snapshot.size == 6 + numberOfMessagesToSend && + // 7 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 7 + numberOfMessagesToSend && snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) // The timeline is fully loaded @@ -162,7 +174,5 @@ class TimelineForwardPaginationTest : InstrumentedTest { } aliceTimeline.dispose() - - cryptoTestData.cleanUp(commonTestHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index 6e5fed8df9..3ff4572add 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.checkSendOrder import timber.log.Timber import java.util.concurrent.CountDownLatch @@ -48,9 +47,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { */ @Test - fun previousLastForwardTest() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun previousLastForwardTest() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -74,8 +71,12 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { Timber.w(" event ${it.root}") } - // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events) - snapshot.size == 8 + // Ok, we have the 9 first messages of the initial sync (room creation and bob invite and join events) + // create + // join alice + // power_levels, join_rules, history_visibility, guest_access, name + // invite, join bob + snapshot.size == 9 } bobTimeline.addListener(eventsListener) @@ -192,7 +193,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { Timber.w(" event ${it.root}") } - snapshot.size == 44 // 8 + 1 + 35 + snapshot.size == 45 // 9 + 1 + 35 } bobTimeline.addListener(eventsListener) @@ -220,8 +221,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { // Bob can see the first event of the room (so Back pagination has worked) snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - // 8 for room creation item 60 message from Alice - snapshot.size == 68 && // 8 + 60 + // 9 for room creation item 60 message from Alice + snapshot.size == 69 && // 9 + 60U snapshot.checkSendOrder(secondMessage, 30, 0) && snapshot.checkSendOrder(firstMessage, 30, 30) } @@ -238,7 +239,5 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { } bobTimeline.dispose() - - cryptoTestData.cleanUp(commonTestHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index 42f710d7cf..7ed0be927c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -32,8 +32,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @@ -42,9 +41,7 @@ import org.matrix.android.sdk.common.TestConstants class TimelineSimpleBackPaginationTest : InstrumentedTest { @Test - fun timeline_backPaginate_shouldReachEndOfTimeline() { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + fun timeline_backPaginate_shouldReachEndOfTimeline() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val numberOfMessagesToSent = 200 val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) @@ -102,6 +99,5 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { assertEquals(numberOfMessagesToSent, onlySentEvents.size) bobTimeline.dispose() - cryptoTestData.cleanUp(commonTestHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index 02430dda74..87f404b0f1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -30,8 +30,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.model.message.MessageContent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import java.util.concurrent.CountDownLatch /** !! Not working with the new timeline @@ -47,15 +46,12 @@ class TimelineWithManyMembersTest : InstrumentedTest { private const val NUMBER_OF_MEMBERS = 6 } - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - /** * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. */ @Test - fun everyone_should_decrypt_message_in_a_crowded_room() { + fun everyone_should_decrypt_message_in_a_crowded_room() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS) val sessionForFirstMember = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index e17b7efbd6..7c97426c39 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -26,9 +26,8 @@ import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.search.SearchResult -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.CryptoTestHelper @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -74,9 +73,7 @@ class SearchMessagesTest : InstrumentedTest { } } - private fun doTest(block: suspend (CryptoTestData) -> SearchResult) { - val commonTestHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -99,7 +96,5 @@ class SearchMessagesTest : InstrumentedTest { (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() }.orFalse() ) - - cryptoTestData.cleanUp(commonTestHelper) } } 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 b9760c1bfc..0d8a9058a2 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 @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.space.JoinSpaceResult -import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams @RunWith(JUnit4::class) @@ -49,8 +49,7 @@ import org.matrix.android.sdk.common.SessionTestParams class SpaceCreationTest : InstrumentedTest { @Test - fun createSimplePublicSpace() { - val commonTestHelper = CommonTestHelper(context()) + fun createSimplePublicSpace() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" val topic = "A public space for test" @@ -96,13 +95,10 @@ class SpaceCreationTest : InstrumentedTest { ?.toModel()?.historyVisibility assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility) - - commonTestHelper.signOutAndClose(session) } @Test - fun testJoinSimplePublicSpace() { - val commonTestHelper = CommonTestHelper(context()) + fun testJoinSimplePublicSpace() = runSessionTest(context()) { commonTestHelper -> val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) @@ -134,8 +130,7 @@ class SpaceCreationTest : InstrumentedTest { } @Test - fun testSimplePublicSpaceWithChildren() { - val commonTestHelper = CommonTestHelper(context()) + fun testSimplePublicSpaceWithChildren() = runSessionTest(context()) { commonTestHelper -> val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) @@ -204,8 +199,5 @@ class SpaceCreationTest : InstrumentedTest { // ).size // // assertEquals("Unexpected number of joined children", 1, childCount) - - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) } } 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 6a17cb74ad..f16cd91e36 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 @@ -29,8 +29,8 @@ 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.ActiveSpaceFilter import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.query.SpaceFilter 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 @@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams @RunWith(JUnit4::class) @@ -55,8 +56,7 @@ import org.matrix.android.sdk.common.SessionTestParams class SpaceHierarchyTest : InstrumentedTest { @Test - fun createCanonicalChildRelation() { - val commonTestHelper = CommonTestHelper(context()) + fun createCanonicalChildRelation() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" @@ -173,8 +173,7 @@ class SpaceHierarchyTest : InstrumentedTest { // } @Test - fun testFilteringBySpace() { - val commonTestHelper = CommonTestHelper(context()) + fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( @@ -185,12 +184,12 @@ class SpaceHierarchyTest : InstrumentedTest { ) /* val spaceBInfo = */ createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) - ) + session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) + ) val spaceCInfo = createPublicSpace( session, "SpaceC", listOf( @@ -249,15 +248,14 @@ class SpaceHierarchyTest : InstrumentedTest { Thread.sleep(6_000) val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams { - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) + spaceFilter = SpaceFilter.OrphanRooms }) assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size) } @Test @Ignore("This test will be ignored until it is fixed") - fun testBreakCycle() { - val commonTestHelper = CommonTestHelper(context()) + fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( @@ -302,8 +300,7 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test - fun testLiveFlatChildren() { - val commonTestHelper = CommonTestHelper(context()) + fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( @@ -395,25 +392,26 @@ class SpaceHierarchyTest : InstrumentedTest { childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - val commonTestHelper = CommonTestHelper(context()) var spaceId = "" var roomIds: List = emptyList() - commonTestHelper.waitWithLatch { latch -> - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + 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 ?: "") - 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 = 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) + } + } + latch.countDown() } - latch.countDown() } return TestSpaceCreationResult(spaceId, roomIds) } @@ -423,51 +421,51 @@ class SpaceHierarchyTest : InstrumentedTest { childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - val commonTestHelper = CommonTestHelper(context()) var spaceId = "" var roomIds: List = emptyList() - 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) - ) - ) - }) + 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) } - 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() } - latch.countDown() } return TestSpaceCreationResult(spaceId, roomIds) } @Test - fun testRootSpaces() { - val commonTestHelper = CommonTestHelper(context()) + fun testRootSpaces() = 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) - ) - ) + session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) + ) val spaceBInfo = createPublicSpace( session, "SpaceB", listOf( @@ -506,13 +504,10 @@ class SpaceHierarchyTest : InstrumentedTest { } assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) - - commonTestHelper.signOutAndClose(session) } @Test - fun testParentRelation() { - val commonTestHelper = CommonTestHelper(context()) + fun testParentRelation() = runSessionTest(context()) { commonTestHelper -> val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) @@ -604,8 +599,5 @@ class SpaceHierarchyTest : InstrumentedTest { bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true } } - - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 867e066e60..82f39806c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -177,7 +177,7 @@ object MatrixPatterns { * - "@alice:domain.org".getDomain() will return "domain.org" * - "@bob:domain.org:3455".getDomain() will return "domain.org:3455" */ - fun String.getDomain(): String { + fun String.getServerName(): String { if (BuildConfig.DEBUG && !isUserId(this)) { // They are some invalid userId localpart in the wild, but the domain part should be there anyway Timber.w("Not a valid user ID: $this") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt index a34dbcc196..3c376b55ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt @@ -16,6 +16,14 @@ package org.matrix.android.sdk.api +/** + * This interface exists to let the implementation provide localized room display name fallback. + * The methods can be called when the room has no name, i.e. its `m.room.name` state event does not exist or + * the name in it is an empty String. + * It allows the SDK to store the room name fallback into the local storage and so let the client do + * queries on the room name. + * *Limitation*: if the locale of the device changes, the methods will not be called again. + */ interface RoomDisplayNameFallbackProvider { fun getNameForRoomInvite(): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 5a19df90c4..629d947bb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -66,7 +66,7 @@ interface AuthenticationService { /** * True when login and password has been sent with success to the homeserver. */ - val isRegistrationStarted: Boolean + fun isRegistrationStarted(): Boolean /** * Cancel pending login or pending registration. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt index c2c1f043bb..c3f0221bb8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt @@ -195,7 +195,7 @@ data class HomeServerConnectionConfig( * - https://www.ssi.gouv.fr/uploads/2017/07/anssi-guide-recommandations_de_securite_relatives_a_tls-v1.2.pdf * - https://developer.android.com/reference/javax/net/ssl/SSLEngine * - * @param tlsLimitations true to use Tls limitations + * @param tlsLimitations true to use Tls limitations * @param enableCompatibilityMode set to true for Android < 20 * @return this builder */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt index 439b4beb41..9e6b2b3ad9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt @@ -18,13 +18,31 @@ package org.matrix.android.sdk.api.auth.registration import org.matrix.android.sdk.api.session.Session -// Either a session or an object containing data about registration stages +/** + * Either a session or an object containing data about registration stages. + */ sealed class RegistrationResult { + /** + * The registration is successful, the [Session] is provided. + */ data class Success(val session: Session) : RegistrationResult() + + /** + * The registration still miss some steps. See [FlowResult] to know the details. + */ data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() } +/** + * Information about the missing and completed [Stage]. + */ data class FlowResult( + /** + * List of missing stages. + */ val missingStages: List, + /** + * List of completed stages. + */ val completedStages: List ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index 0cda64499f..14d26bf2b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -109,14 +109,14 @@ interface RegistrationWizard { suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult /** - * This is the current ThreePid, waiting for validation. The SDK will store it in database, so it can be + * Returns the current ThreePid, waiting for validation. The SDK will store it in database, so it can be * restored even if the app has been killed during the registration */ - val currentThreePid: String? + fun getCurrentThreePid(): String? /** - * True when login and password have been sent with success to the homeserver, i.e. [createAccount] has been + * Return true when login and password have been sent with success to the homeserver, i.e. [createAccount] has been * called successfully. */ - val isRegistrationStarted: Boolean + fun isRegistrationStarted(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt index c21b667cf7..281b0c2808 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt @@ -16,25 +16,40 @@ package org.matrix.android.sdk.api.auth.registration +/** + * Registration stages. + */ sealed class Stage(open val mandatory: Boolean) { - // m.login.recaptcha + /** + * m.login.recaptcha stage. + */ data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) - // m.login.email.identity + /** + * m.login.email.identity stage. + */ data class Email(override val mandatory: Boolean) : Stage(mandatory) - // m.login.msisdn + /** + * m.login.msisdn stage. + */ data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) - // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username - // and a password, the dummy stage has to be done + /** + * m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username + * and a password, the dummy stage has to be done. + */ data class Dummy(override val mandatory: Boolean) : Stage(mandatory) - // Undocumented yet: m.login.terms + /** + * Undocumented yet: m.login.terms stage. + */ data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) - // For unknown stages + /** + * For unknown stages. + */ data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt index 2880d851d6..ddf76d6e42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt @@ -17,13 +17,19 @@ package org.matrix.android.sdk.api.cache sealed class CacheStrategy { - // Data is always fetched from the server + /** + * Data is always fetched from the server. + */ object NoCache : CacheStrategy() - // Once data is retrieved, it is stored for the provided amount of time. - // In case of error, and if strict is set to false, the cache can be returned if available + /** + * Once data is retrieved, it is stored for the provided amount of time. + * In case of error, and if strict is set to false, the cache can be returned if available + */ data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy() - // Once retrieved, the data is stored in cache and will be always get from the cache + /** + * Once retrieved, the data is stored in cache and will be always get from the cache. + */ object InfiniteCache : CacheStrategy() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt index be139fd82b..7d4f553bed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -37,7 +37,9 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) - // When server send an error, but it cannot be interpreted as a MatrixError + /** + * When server send an error, but it cannot be interpreted as a MatrixError. + */ data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt index 368ff98661..f08c86885d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -20,20 +20,52 @@ package org.matrix.android.sdk.api.query * Basic query language. All these cases are mutually exclusive. */ sealed interface QueryStringValue { + /** + * No condition, i.e. there will be no test on the tested field. + */ + object NoCondition : QueryStringValue + + /** + * The tested field has to be null. + */ + object IsNull : QueryStringValue + + /** + * The tested field has to be not null. + */ + object IsNotNull : QueryStringValue + + /** + * The tested field has to be empty. + */ + object IsEmpty : QueryStringValue + + /** + * The tested field has to not empty. + */ + object IsNotEmpty : QueryStringValue + + /** + * Interface to check String content. + */ sealed interface ContentQueryStringValue : QueryStringValue { val string: String val case: Case } - object NoCondition : QueryStringValue - object IsNull : QueryStringValue - object IsNotNull : QueryStringValue - object IsEmpty : QueryStringValue - object IsNotEmpty : QueryStringValue - + /** + * The tested field must match the [string]. + */ data class Equals(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + + /** + * The tested field must contain the [string]. + */ data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + /** + * Case enum for [ContentQueryStringValue]. + */ enum class Case { /** * Match query sensitive to case. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt index c8ccc4c8a3..c2117adbd3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt @@ -16,9 +16,23 @@ package org.matrix.android.sdk.api.query +/** + * To filter by Room category. + * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams] + */ enum class RoomCategoryFilter { + /** + * Get only the DM, i.e. the rooms referenced in `m.direct` account data. + */ ONLY_DM, + + /** + * Get only the Room, not the DM, i.e. the rooms not referenced in `m.direct` account data. + */ ONLY_ROOMS, + + /** + * Get the room with non-0 notifications. + */ ONLY_WITH_NOTIFICATIONS, - ALL } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt index 613916bc18..73947f8f7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt @@ -16,8 +16,22 @@ package org.matrix.android.sdk.api.query +/** + * Filter room by their tag. + * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams] + * @see [org.matrix.android.sdk.api.session.room.model.tag.RoomTag] + */ data class RoomTagQueryFilter( + /** + * Set to true to get the rooms which have the tag "m.favourite". + */ val isFavorite: Boolean?, + /** + * Set to true to get the rooms which have the tag "m.lowpriority". + */ val isLowPriority: Boolean?, - val isServerNotice: Boolean? + /** + * Set to true to get the rooms which have the tag "m.server_notice". + */ + val isServerNotice: Boolean?, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt new file mode 100644 index 0000000000..6383412ffb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 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.query + +/** + * Filter to be used to do room queries regarding the space hierarchy. + * @see [org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams] + */ +sealed interface SpaceFilter { + /** + * Used to get all the rooms that are not in any space. + */ + object OrphanRooms : SpaceFilter + + /** + * Used to get all the rooms that have the provided space in their parent hierarchy. + */ + data class ActiveSpace(val spaceId: String) : SpaceFilter + + /** + * Used to get all the rooms that do not have the provided space in their parent hierarchy. + */ + data class ExcludeSpace(val spaceId: String) : SpaceFilter +} + +/** + * Return a [SpaceFilter.ActiveSpace] if the String is not null, or [SpaceFilter.OrphanRooms]. + */ +fun String?.toActiveSpaceOrOrphanRooms(): SpaceFilter = this?.let { SpaceFilter.ActiveSpace(it) } ?: SpaceFilter.OrphanRooms diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt index e59e676ed9..20f977e86e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt @@ -36,7 +36,7 @@ interface ContentUrlResolver { /** * Get the actual URL for accessing the full-size image of a Matrix media content URI. * - * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). * @return the URL to access the described resource, or null if the url is invalid. */ fun resolveFullSize(contentUrl: String?): String? @@ -44,7 +44,7 @@ interface ContentUrlResolver { /** * Get the ResolvedMethod to download a URL. * - * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). * @param elementToDecrypt Encryption data may be required if you use a content scanner * @return the Method to access resource, or null if invalid */ @@ -54,9 +54,9 @@ interface ContentUrlResolver { * Get the actual URL for accessing the thumbnail image of a given Matrix media content URI. * * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). - * @param width the desired width - * @param height the desired height - * @param method the desired method (METHOD_CROP or METHOD_SCALE) + * @param width the desired width + * @param height the desired height + * @param method the desired method (METHOD_CROP or METHOD_SCALE) * @return the URL to access the described resource, or null if the url is invalid. */ fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt index 7a85a89058..22250628d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt @@ -33,7 +33,7 @@ interface ContentScannerService { /** * Get the current public curve25519 key that the AV server is advertising. - * @param callback on success callback containing the server public key + * @param forceDownload true to force the SDK to download again the server public key */ suspend fun getServerPublicKey(forceDownload: Boolean = false): String? suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt index 0d40490c3e..dc8b03eaae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt @@ -34,7 +34,7 @@ interface KeysBackupService { * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion]. * * @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion]. - * @param callback Asynchronous callback + * @param callback Asynchronous callback */ fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback) @@ -122,7 +122,7 @@ interface KeysBackupService { * Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself. * If we are backing up to this version. Backup will be stopped. * - * @param version the backup version to delete. + * @param version the backup version to delete. * @param callback Asynchronous callback */ fun deleteBackup(version: String, @@ -168,17 +168,15 @@ interface KeysBackupService { password: String, callback: MatrixCallback) - fun onSecretKeyGossip(secret: String) - /** * Restore a backup with a recovery key from a given backup version stored on the homeserver. * - * @param keysVersionResult the backup version to restore from. - * @param recoveryKey the recovery key to decrypt the retrieved backup. - * @param roomId the id of the room to get backup data from. - * @param sessionId the id of the session to restore. + * @param keysVersionResult the backup version to restore from. + * @param recoveryKey the recovery key to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. * @param stepProgressListener the step progress listener - * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. */ fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, @@ -204,10 +202,13 @@ interface KeysBackupService { callback: MatrixCallback) val keysBackupVersion: KeysVersionResult? + val currentBackupVersion: String? - val isEnabled: Boolean - val isStucked: Boolean - val state: KeysBackupState + get() = keysBackupVersion?.version + + fun isEnabled(): Boolean + fun isStuck(): Boolean + fun getState(): KeysBackupState // For gossiping fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt index a4cc133398..a867d573de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt @@ -51,33 +51,51 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup * */ enum class KeysBackupState { - // Need to check the current backup version on the homeserver + /** + * Need to check the current backup version on the homeserver. + */ Unknown, - // Checking if backup is enabled on homeserver + /** + * Checking if backup is enabled on homeserver. + */ CheckingBackUpOnHomeserver, - // Backup has been stopped because a new backup version has been detected on the homeserver + /** + * Backup has been stopped because a new backup version has been detected on the homeserver. + */ WrongBackUpVersion, - // Backup from this device is not enabled + /** + * Backup from this device is not enabled. + */ Disabled, - // There is a backup available on the homeserver but it is not trusted. - // It is not trusted because the signature is invalid or the device that created it is not verified - // Use [KeysBackup.getKeysBackupTrust()] to get trust details. - // Consequently, the backup from this device is not enabled. + /** + * There is a backup available on the homeserver but it is not trusted. + * It is not trusted because the signature is invalid or the device that created it is not verified. + * Use [KeysBackup.getKeysBackupTrust()] to get trust details. + * Consequently, the backup from this device is not enabled. + */ NotTrusted, - // Backup is being enabled: the backup version is being created on the homeserver + /** + * Backup is being enabled: the backup version is being created on the homeserver. + */ Enabling, - // Backup is enabled and ready to send backup to the homeserver + /** + * Backup is enabled and ready to send backup to the homeserver. + */ ReadyToBackUp, - // e2e keys are going to be sent to the homeserver + /** + * e2e keys are going to be sent to the homeserver. + */ WillBackUp, - // e2e keys are being sent to the homeserver + /** + * e2e keys are being sent to the homeserver. + */ BackingUp } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt index 0c19d275cc..4ff196dd07 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt @@ -48,8 +48,7 @@ data class IncomingRoomKeyRequest( /** * Factory. * - * @param event the event - * @param currentTimeMillis the current time in milliseconds + * @param trail the AuditTrail data */ fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? { return trail diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt index 744fe74d0d..736ae6b318 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt @@ -46,8 +46,8 @@ class MXUsersDevicesMap { /** * Provides the object for a device id and a user Id. * + * @param userId the user id * @param deviceId the device id - * @param userId the object id * @return the object */ fun getObject(userId: String?, deviceId: String?): E? { @@ -59,9 +59,9 @@ class MXUsersDevicesMap { /** * Set an object for a dedicated user Id and device Id. * - * @param userId the user Id + * @param userId the user Id * @param deviceId the device id - * @param o the object to set + * @param o the object to set */ fun setObject(userId: String?, deviceId: String?, o: E?) { if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) { @@ -73,8 +73,8 @@ class MXUsersDevicesMap { /** * Defines the objects map for a user Id. * + * @param userId the user id * @param objectsPerDevices the objects maps - * @param userId the user id */ fun setObjects(userId: String?, objectsPerDevices: Map?) { if (!userId.isNullOrBlank()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt index 68c7496d58..78724819a3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt @@ -20,15 +20,23 @@ package org.matrix.android.sdk.api.session.crypto.model * RoomEncryptionTrustLevel represents the trust level in an encrypted room. */ enum class RoomEncryptionTrustLevel { - // No one in the room has been verified -> Black shield + /** + * No one in the room has been verified -> Black shield. + */ Default, - // There are one or more device un-verified -> the app should display a red shield + /** + * There are one or more device un-verified -> the app should display a red shield. + */ Warning, - // All devices in the room are verified -> the app should display a green shield + /** + * All devices in the room are verified -> the app should display a green shield. + */ Trusted, - // e2e is active but with an unsupported algorithm + /** + * e2e is active but with an unsupported algorithm. + */ E2EWithUnsupportedAlgorithm } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt index 5a025f37e1..e4716d7794 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt @@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) { MismatchedKeys("m.key_mismatch", "Key mismatch"), UserError("m.user_error", "User error"), MismatchedUser("m.user_mismatch", "User mismatch"), - QrCodeInvalid("m.qr_code.invalid", "Invalid QR code") + QrCodeInvalid("m.qr_code.invalid", "Invalid QR code"), + AcceptedByAnotherDevice("m.accepted", "Verification request accepted by another device") } fun safeValueOf(code: String?): CancelCode { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt index f2de2c4b47..0ab47a2ecd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt @@ -20,12 +20,18 @@ package org.matrix.android.sdk.api.session.crypto.verification * Verification methods. */ enum class VerificationMethod { - // Use it when your application supports the SAS verification method + /** + * Use it when your application supports the SAS verification method. + */ SAS, - // Use it if your application is able to display QR codes + /** + * Use it if your application is able to display QR codes. + */ QR_CODE_SHOW, - // Use it if your application is able to scan QR codes + /** + * Use it if your application is able to scan QR codes. + */ QR_CODE_SCAN } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt index 39de2cc712..30e4c66937 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt @@ -17,10 +17,14 @@ package org.matrix.android.sdk.api.session.crypto.verification sealed class VerificationTxState { - // Uninitialized state + /** + * Uninitialized state. + */ object None : VerificationTxState() - // Specific for SAS + /** + * Specific for SAS. + */ abstract class VerificationSasTxState : VerificationTxState() object SendingStart : VerificationSasTxState() @@ -38,18 +42,26 @@ sealed class VerificationTxState { object MacSent : VerificationSasTxState() object Verifying : VerificationSasTxState() - // Specific for QR code + /** + * Specific for QR code. + */ abstract class VerificationQrTxState : VerificationTxState() - // Will be used to ask the user if the other user has correctly scanned + /** + * Will be used to ask the user if the other user has correctly scanned. + */ object QrScannedByOther : VerificationQrTxState() object WaitingOtherReciprocateConfirm : VerificationQrTxState() - // Terminal states + /** + * Terminal states. + */ abstract class TerminalTxState : VerificationTxState() object Verified : TerminalTxState() - // Cancelled by me or by other + /** + * Cancelled by me or by other. + */ data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 16bdbd3432..7124d8a1a3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent 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.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -375,11 +376,11 @@ fun Event.getRelationContent(): RelationDefaultContent? { content.toModel()?.relatesTo } else { content.toModel()?.relatesTo ?: run { - // Special case to handle stickers, while there is only a local msgtype for stickers - if (getClearType() == EventType.STICKER) { - getClearContent().toModel()?.relatesTo - } else { - null + // Special cases when there is only a local msgtype for some event types + when (getClearType()) { + EventType.STICKER -> getClearContent().toModel()?.relatesTo + in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel()?.relatesTo + else -> null } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index 84a9990826..a7c81136e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -33,7 +33,7 @@ interface FileService { /** * The original file is in cache, but the decrypted files can be deleted for security reason. * To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again - * @param decryptedFileInCache true if the decrypted file is available. Always true for clear files. + * @property decryptedFileInCache true if the decrypted file is available. Always true for clear files. */ data class InCache(val decryptedFileInCache: Boolean) : FileState() object Downloading : FileState() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt index c03b42e6c8..2fb35d38e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -74,6 +74,7 @@ interface IdentityService { /** * Submit the code that the identity server has sent to the user (in email or SMS). * Once successful, you will have to call [finalizeBindThreePid] + * @param threePid the three pid * @param code the code sent to the user */ suspend fun submitValidationToken(threePid: ThreePid, code: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt index 60af93888e..5b15a0cb13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt @@ -99,6 +99,7 @@ interface IntegrationManagerService { * Offers to allow or disallow a native widget domain. * @param widgetType the widget type to check for * @param domain the domain to check for + * @param allowed true or false */ suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt index c5d919407a..c428e40203 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt @@ -29,6 +29,7 @@ object MatrixLinkify { * Find the matrix spans i.e matrix id , user id ... to display them as URL. * * @param spannable the text in which the matrix items has to be clickable. + * @param callback listener to be notified when the span is clicked */ @Suppress("UNUSED_PARAMETER") fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixPermalinkSpan.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixPermalinkSpan.kt index 2f8f5f99a5..48b30dfa21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixPermalinkSpan.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixPermalinkSpan.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.permalinks.MatrixPermalinkSpan.Callbac /** * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. - * @param url the permalink url tied to the span - * @param callback the callback to use. + * @property url the permalink url tied to the span + * @property callback the callback to use. */ class MatrixPermalinkSpan(private val url: String, private val callback: Callback? = null) : ClickableSpan() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index b49b80df09..1788bf7bd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -60,6 +60,7 @@ interface PermalinkService { * Creates a permalink for a roomId, including the via parameters. * * @param roomId the room id + * @param viaServers the via parameter * @param forceMatrixTo whether we should force using matrix.to base URL * * @return the permalink, or null in case of error @@ -70,7 +71,7 @@ interface PermalinkService { * Creates a permalink for an event. If you have an event you can use [createPermalink] * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org?via=matrix.org" * - * @param roomId the id of the room + * @param roomId the id of the room * @param eventId the id of the event * @param forceMatrixTo whether we should force using matrix.to base URL * @@ -90,7 +91,7 @@ interface PermalinkService { * Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user. * Ex: "%2\$s" or "[%2\$s](https://matrix.to/#/%1\$s)" * - * @param type: type of template to create + * @param type type of template to create * @param forceMatrixTo whether we should force using matrix.to base URL * * @return the created template diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt new file mode 100644 index 0000000000..1ae23e2b70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.pushers + +data class HttpPusher( + /** + * This is a unique identifier for this pusher. The value you should use for + * this is the routing or destination address information for the notification, + * for example, the APNS token for APNS or the Registration ID for GCM. If your + * notification client has no such concept, use any unique identifier. Max length, 512 chars. + */ + val pushkey: String, + + /** + * The application id + * This is a reverse-DNS style identifier for the application. It is recommended + * that this end with the platform, such that different platform versions get + * different app identifiers. Max length, 64 chars. + */ + val appId: String, + + /** + * This string determines which set of device specific rules this pusher executes. + */ + val profileTag: String, + + /** + * The preferred language for receiving notifications (e.g. "en" or "en-US"). + */ + val lang: String, + + /** + * A human readable string that will allow the user to identify what application owns this pusher. + */ + val appDisplayName: String, + + /** + * A human readable string that will allow the user to identify what device owns this pusher. + */ + val deviceDisplayName: String, + + /** + * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + */ + val url: String, + + /** + * If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. + */ + val append: Boolean, + + /** + * true to limit the push content to only id and not message content + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour + */ + val withEventIdOnly: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index 5f9857eb2f..396f13ce46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -47,12 +47,12 @@ interface PushersService { * Add a new Email pusher. * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set * - * @param email The email address to send notifications to. - * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). - * @param emailBranding The branding placeholder to include in the email communications. - * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. + * @param email The email address to send notifications to. + * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). + * @param emailBranding The branding placeholder to include in the email communications. + * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. - * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers * with the same App ID and pushkey for different users. Typically We always want to append for * email pushers since we don't want to stop other accounts notifying to the same email address. @@ -107,61 +107,4 @@ interface PushersService { * Get the current pushers. */ fun getPushers(): List - - data class HttpPusher( - - /** - * This is a unique identifier for this pusher. The value you should use for - * this is the routing or destination address information for the notification, - * for example, the APNS token for APNS or the Registration ID for GCM. If your - * notification client has no such concept, use any unique identifier. Max length, 512 chars. - */ - val pushkey: String, - - /** - * The application id - * This is a reverse-DNS style identifier for the application. It is recommended - * that this end with the platform, such that different platform versions get - * different app identifiers. Max length, 64 chars. - */ - val appId: String, - - /** - * This string determines which set of device specific rules this pusher executes. - */ - val profileTag: String, - - /** - * The preferred language for receiving notifications (e.g. "en" or "en-US"). - */ - val lang: String, - - /** - * A human readable string that will allow the user to identify what application owns this pusher. - */ - val appDisplayName: String, - - /** - * A human readable string that will allow the user to identify what device owns this pusher. - */ - val deviceDisplayName: String, - - /** - * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. - */ - val url: String, - - /** - * If true, the homeserver should add another pusher with the given pushkey and App ID in addition - * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers - * with the same App ID and pushkey for different users. - */ - val append: Boolean, - - /** - * true to limit the push content to only id and not message content - * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour - */ - val withEventIdOnly: Boolean - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/PushRuleService.kt index bc4860be11..7ffbc89559 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/PushRuleService.kt @@ -34,10 +34,11 @@ interface PushRuleService { /** * Enables/Disables a push rule and updates the actions if necessary. + * @param kind the rule kind + * @param ruleId the rule id * @param enable Enables/Disables the rule * @param actions Actions to update if not null */ - suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List?) suspend fun removePushRule(kind: RuleKind, ruleId: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt index 5bf42b8252..9498ed002c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/RuleSet.kt @@ -67,7 +67,7 @@ data class RuleSet( /** * Find a rule from its rule Id. * - * @param rules the rules list. + * @param rules the rules list. * @param ruleId the rule Id. * @return the bing rule if it exists, else null. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 3a18cf1497..5d2769ac3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService @@ -163,4 +164,9 @@ interface Room { * Get the RoomVersionService associated to this Room. */ fun roomVersionService(): RoomVersionService + + /** + * Get the LocationSharingService associated to this Room. + */ + fun locationSharingService(): LocationSharingService } 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 6d5551ddf0..01776ca66d 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 @@ -97,6 +97,12 @@ interface RoomService { */ fun getRoomSummary(roomIdOrAlias: String): RoomSummary? + /** + * A live [RoomSummary] associated with the room with id [roomId]. + * You can observe this summary to get dynamic data from this room, even if the room is not joined yet + */ + fun getRoomSummaryLive(roomId: String): LiveData> + /** * Get a snapshot list of room summaries. * @return the immutable list of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt index e721abd6a0..9368ad6bf4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt @@ -16,9 +16,28 @@ package org.matrix.android.sdk.api.session.room +/** + * Enum to sort room list. + */ enum class RoomSortOrder { + /** + * Sort room list by room ascending name. + */ NAME, + + /** + * Sort room list by room descending last activity. + */ ACTIVITY, + + /** + * Sort room list by room priority and last activity: favorite room first, low priority room last, + * then descending last activity. + */ PRIORITY_AND_ACTIVITY, + + /** + * Do not sort room list. Useful if the order does not matter. Order can be indeterminate. + */ NONE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 5c74dcced1..3d943473e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -16,60 +16,99 @@ package org.matrix.android.sdk.api.session.room -import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter +import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams +/** + * Create a [RoomSummaryQueryParams] object, calling [init] with a [RoomSummaryQueryParams.Builder]. + */ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { - return RoomSummaryQueryParams.Builder().apply(init).build() -} - -fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams { return RoomSummaryQueryParams.Builder() .apply(init) - .apply { - includeType = listOf(RoomType.SPACE) - excludeType = null - roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS - } .build() } /** - * This class can be used to filter room summaries to use with: - * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]. + * Create a [SpaceSummaryQueryParams] object (which is a [RoomSummaryQueryParams]), calling [init] with a [RoomSummaryQueryParams.Builder]. + * This is specific for spaces, other filters will be applied after invoking [init] + */ +fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams { + return roomSummaryQueryParams { + init() + includeType = listOf(RoomType.SPACE) + excludeType = null + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } +} + +/** + * This class can be used to filter room summaries to use with [RoomService]. + * It provides a [Builder]. + * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of this class. */ data class RoomSummaryQueryParams( - val roomId: QueryStringValue, + /** + * Query for the displayName of the room. The display name can be the value of the state event, + * or a value returned by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider]. + */ val displayName: QueryStringValue, + /** + * Query for the canonical alias of the room. + */ val canonicalAlias: QueryStringValue, + /** + * Used to filter room by membership. + */ val memberships: List, + /** + * Used to filter room by room category. + */ val roomCategoryFilter: RoomCategoryFilter?, + /** + * Used to filter room by room tag. + */ val roomTagQueryFilter: RoomTagQueryFilter?, + /** + * Used to filter room by room type. + * @see [includeType] + */ val excludeType: List?, + /** + * Used to filter room by room type. + * @see [excludeType] + */ val includeType: List?, - val activeSpaceFilter: ActiveSpaceFilter?, + /** + * Used to filter room using the current space. + */ + val spaceFilter: SpaceFilter?, + /** + * Used to filter room using the current group. + */ val activeGroupId: String? = null ) { + /** + * Builder for [RoomSummaryQueryParams]. + * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of [RoomSummaryQueryParams]. + */ class Builder { - var roomId: QueryStringValue = QueryStringValue.IsNotEmpty - var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var displayName: QueryStringValue = QueryStringValue.NoCondition var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var memberships: List = Membership.all() - var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL + var roomCategoryFilter: RoomCategoryFilter? = null var roomTagQueryFilter: RoomTagQueryFilter? = null var excludeType: List? = listOf(RoomType.SPACE) var includeType: List? = null - var activeSpaceFilter: ActiveSpaceFilter = ActiveSpaceFilter.None + var spaceFilter: SpaceFilter? = null var activeGroupId: String? = null fun build() = RoomSummaryQueryParams( - roomId = roomId, displayName = displayName, canonicalAlias = canonicalAlias, memberships = memberships, @@ -77,7 +116,7 @@ data class RoomSummaryQueryParams( roomTagQueryFilter = roomTagQueryFilter, excludeType = excludeType, includeType = includeType, - activeSpaceFilter = activeSpaceFilter, + spaceFilter = spaceFilter, activeGroupId = activeGroupId ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index 6967e0c455..6064643820 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -28,7 +28,8 @@ interface RoomCryptoService { /** * Enable encryption of the room. - * @param Use force to ensure that this algorithm will be used. Otherwise this call + * @param algorithm the algorithm to set, default to [MXCRYPTO_ALGORITHM_MEGOLM] + * @param force Use force to ensure that this algorithm will be used. Otherwise this call * will throw if encryption is already setup or if the algorithm is not supported. Only to * be used by admins to fix misconfigured encryption. */ 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 new file mode 100644 index 0000000000..dd48d51f45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.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.api.session.room.location + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary + +/** + * Manage all location sharing related features. + */ +interface LocationSharingService { + fun getRunningLiveLocationShareSummaries(): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index 71c1d8303e..1ab23b7a11 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -28,65 +28,200 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] */ data class RoomSummary( + /** + * The roomId of the room. + */ val roomId: String, - // Computed display name + /** + * Computed display name. The value of the state event `m.room.name` if not empty, else can be the value returned + * by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider]. + */ val displayName: String = "", + /** + * The value of the live state event `m.room.name`. + */ val name: String = "", + /** + * The value of the live state event `m.room.topic`. + */ val topic: String = "", + /** + * The value of the live state event `m.room.avatar`. + */ val avatarUrl: String = "", + /** + * The value of the live state event `m.room.canonical_alias`. + */ val canonicalAlias: String? = null, + /** + * The list of all the aliases of this room. Content of the live state event `m.room.aliases`. + */ val aliases: List = emptyList(), + /** + * The value of the live state event `m.room.join_rules`. + */ val joinRules: RoomJoinRules? = null, + /** + * True is this room is referenced in the account data `m.direct`. + */ val isDirect: Boolean = false, + /** + * If [isDirect] is true, this is the id of the first other member of this room. + */ val directUserId: String? = null, + /** + * If [isDirect] is true, this it the presence of the first other member of this room. + */ val directUserPresence: UserPresence? = null, + /** + * Number of members who have joined this room. + */ val joinedMembersCount: Int? = 0, + /** + * Number of members who are invited to this room. + */ val invitedMembersCount: Int? = 0, + /** + * Latest [TimelineEvent] which can be displayed in this room. Can be used in the room list. + */ val latestPreviewableEvent: TimelineEvent? = null, + /** + * List of other member ids of this room. + */ val otherMemberIds: List = emptyList(), + /** + * Number of unread message in this room. + */ val notificationCount: Int = 0, + /** + * Number of unread and highlighted message in this room. + */ val highlightCount: Int = 0, + /** + * True if this room has unread messages. + */ val hasUnreadMessages: Boolean = false, + /** + * List of tags in this room. + */ val tags: List = emptyList(), + /** + * Current user membership in this room. + */ val membership: Membership = Membership.NONE, + /** + * Versioning state of this room. + */ val versioningState: VersioningState = VersioningState.NONE, + /** + * Value of `m.fully_read` for this room. + */ val readMarkerId: String? = null, + /** + * Message saved as draft for this room. + */ val userDrafts: List = emptyList(), + /** + * True if this room is encrypted. + */ val isEncrypted: Boolean, + /** + * Timestamp of the `m.room.encryption` state event. + */ val encryptionEventTs: Long?, + /** + * List of users who are currently typing on this room. + */ val typingUsers: List, + /** + * UserId of the user who has invited the current user to this room. + */ val inviterId: String? = null, + /** + * Breadcrumb index, util to sort rooms by last seen. + */ val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, + /** + * The room encryption trust level. + * @see [RoomEncryptionTrustLevel] + */ val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, + /** + * True if a message has not been sent in this room. + */ val hasFailedSending: Boolean = false, + /** + * The type of the room. Null for regular room. + * @see [RoomType] + */ val roomType: String? = null, + /** + * List of parent spaces. + */ val spaceParents: List? = null, + /** + * List of children space. + */ val spaceChildren: List? = null, + /** + * List of all the space parents. Will be empty by default, you have to explicitly request it. + */ + val flattenParents: List = emptyList(), + /** + * List of all the space parent Ids. + */ val flattenParentIds: List = emptyList(), - val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null + /** + * Information about the encryption algorithm, if this room is encrypted. + */ + val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null, ) { - + /** + * True if [versioningState] is not [VersioningState.NONE]. + */ val isVersioned: Boolean get() = versioningState != VersioningState.NONE + /** + * True if [notificationCount] is not `0`. + */ val hasNewMessages: Boolean get() = notificationCount != 0 + /** + * True if the room has the tag `m.lowpriority`. + */ val isLowPriority: Boolean get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY) + /** + * True if the room has the tag `m.favourite`. + */ val isFavorite: Boolean get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE) + /** + * True if [joinRules] is [RoomJoinRules.PUBLIC]. + */ val isPublic: Boolean get() = joinRules == RoomJoinRules.PUBLIC + /** + * Test if the room has the provided [tag]. + */ fun hasTag(tag: String) = tags.any { it.name == tag } + /** + * True if a 1-1 call can be started, i.e. the room has exactly 2 joined members. + */ val canStartCall: Boolean get() = joinedMembersCount == 2 companion object { + /** + * Constant to indicated that the room is not on the breadcrumbs. + * Used by [breadcrumbsIndex]. + */ const val NOT_IN_BREADCRUMBS = -1 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt index b4e7b10d44..2e1668ebbb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt @@ -16,8 +16,22 @@ package org.matrix.android.sdk.api.session.room.model +/** + * Enum for the versioning state of a room. + */ enum class VersioningState { + /** + * The room is not versioned. + */ NONE, + + /** + * The room has been upgraded, but the new room is not joined yet. + */ UPGRADED_ROOM_NOT_JOINED, - UPGRADED_ROOM_JOINED + + /** + * The room has been upgraded, and the new room has been joined. + */ + UPGRADED_ROOM_JOINED, } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt index 0b28d62f56..5ad1a48217 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt @@ -22,6 +22,10 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati * Aggregation info concerning a live location share. */ data class LiveLocationShareAggregatedSummary( + val userId: String?, + /** + * Indicate whether the live is currently running. + */ val isActive: Boolean?, val endOfLiveTimestampMillis: Long?, val lastLocationDataContent: MessageBeaconLocationDataContent?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 0d094b835b..02c597ee63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -71,8 +71,8 @@ interface RelationService { /** * Edit a poll. - * @param pollType indicates open or closed polls * @param targetEvent The poll event to edit + * @param pollType indicates open or closed polls * @param question The edited question * @param options The edited options */ @@ -84,7 +84,9 @@ interface RelationService { /** * Edit a text message body. Limited to "m.text" contentType. * @param targetEvent The event to edit + * @param msgType the message type * @param newBodyText The edited body + * @param newBodyAutoMarkdown true to parse markdown on the new body * @param compatibilityBodyText The text that will appear on clients that don't support yet edition */ fun editTextMessage(targetEvent: TimelineEvent, @@ -153,8 +155,8 @@ interface RelationService { * @param rootThreadEventId the root thread eventId * @param replyInThreadText the reply text * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE - * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param eventReplied the event referenced by the reply within a thread */ fun replyInThread(rootThreadEventId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt index 165a912b7f..36993074aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -58,7 +58,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { /** * Tell if an user can send an event of a certain type. * - * @param userId the id of the user to check for. + * @param userId the id of the user to check for. * @param isState true if the event is a state event (ie. state key is not null) * @param eventType the event type to check for * @return true if the user can send this type of event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index 036628c02f..dac1a1a773 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -71,7 +71,7 @@ interface ReadService { /** * Returns a live list of read receipts for a given event. - * @param eventId: the event + * @param eventId the event */ fun getEventReadReceiptsLive(eventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 4bb8abef8a..c2e3ded2fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -62,6 +62,7 @@ interface SendService { * @param quotedEvent The event to which we will quote it's content. * @param text the text message to send * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param rootThreadEventId when this param is not null, the message will be sent in this specific thread * @return a [Cancelable] */ fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt index 7c806bf35b..d058ff2840 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt @@ -17,27 +17,44 @@ package org.matrix.android.sdk.api.session.room.send enum class SendState { + /** + * The state is unknown. + */ UNKNOWN, - // the event has not been sent + /** + * The event has not been sent. + */ UNSENT, - // the event is encrypting + /** + * The event is encrypting. + */ ENCRYPTING, - // the event is currently sending + /** + * The event is currently sending. + */ SENDING, - // the event has been sent + /** + * The event has been sent. + */ SENT, - // the event has been received from server + /** + * The event has been received from server. + */ SYNCED, - // The event failed to be sent + /** + * The event failed to be sent. + */ UNDELIVERED, - // the event failed to be sent because some unknown devices have been found while encrypting it + /** + * The event failed to be sent because some unknown devices have been found while encrypting it. + */ FAILED_UNKNOWN_DEVICES; internal companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index f6b56128d3..c79171f156 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -90,23 +90,29 @@ interface StateService { /** * Get a state event of the room. + * @param eventType An eventType. + * @param stateKey the query which will be done on the stateKey */ fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? /** * Get a live state event of the room. + * @param eventType An eventType. + * @param stateKey the query which will be done on the stateKey */ fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> /** * Get state events of the room. * @param eventTypes Set of eventType. If empty, all state events will be returned + * @param stateKey the query which will be done on the stateKey */ fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): List /** * Get live state events of the room. * @param eventTypes Set of eventType to observe. If empty, all state events will be observed + * @param stateKey the query which will be done on the stateKey */ fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index b87bc25435..d4ade9b5b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -89,6 +90,7 @@ data class TimelineEvent( /** * Get the metadata associated with a key. + * @param T type to cast the metadata to * @param key the key to get the metadata * @return the metadata */ @@ -140,6 +142,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? { EventType.STICKER -> root.getClearContent().toModel() in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt new file mode 100644 index 0000000000..5a1bf67fdd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyRef.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.securestorage + +data class KeyRef( + val keyId: String?, + val keySpec: SsssKeySpec? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt index 528e071966..929463563f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -92,7 +92,7 @@ interface SharedSecretStorageService { * Clients MUST ensure that the key is trusted before using it to encrypt secrets. * * @param name The name of the secret - * @param secret The secret contents. + * @param secretBase64 The secret contents. * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. */ suspend fun storeSecret(name: String, secretBase64: String, keys: List) @@ -132,9 +132,4 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult suspend fun requestSecret(name: String, myOtherDeviceId: String) - - data class KeyRef( - val keyId: String?, - val keySpec: SsssKeySpec? - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 8f16b3b9c3..38e55664d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -44,8 +44,8 @@ interface SpaceService { roomAliasLocalPart: String? = null): String /** - * Get a space from a roomId. - * @param spaceId the roomId to look for. + * Get a space from a spaceId. + * @param spaceId the spaceId to look for. * @return a space with spaceId or null if room type is not space */ fun getSpace(spaceId: String): Space? @@ -54,21 +54,24 @@ interface SpaceService { * Try to resolve (peek) rooms and subspace in this space. * Use this call get preview of children of this space, particularly useful to get a * preview of rooms that you did not join yet. + * @param spaceId the spaceId to look for. */ suspend fun peekSpace(spaceId: String): SpacePeekResult /** * Get's information of a space by querying the server. + * + * @param spaceId the spaceId to look for. * @param suggestedOnly If true, return only child events and rooms where the m.space.child event has suggested: true. * @param limit a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer. - * @param from: Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided, + * @param from Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided, * then the parameters given for suggested_only and max_depth must be the same. + * @param knownStateList when paginating, pass back the m.space.child state events */ suspend fun querySpaceChildren(spaceId: String, suggestedOnly: Boolean? = null, limit: Int? = null, from: String? = null, - // when paginating, pass back the m.space.child state events knownStateList: List? = null): SpaceHierarchyData /** @@ -98,7 +101,10 @@ interface SpaceService { /** * Let this room declare that it has a parent. + * @param childRoomId the space to set as a child + * @param parentSpaceId the parentId which will be set * @param canonical true if it should be the main parent of this room + * @param viaServers list of candidate servers that can be used to set the parent * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt index c110802d23..6825f8c279 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt @@ -20,14 +20,19 @@ package org.matrix.android.sdk.api.session.threads * This class defines the state of a thread notification. */ enum class ThreadNotificationState { - - // There are no new message + /** + * There are no new message. + */ NO_NEW_MESSAGE, - // There is at least one new message + /** + * There is at least one new message. + */ NEW_MESSAGE, - // The is at least one new message that should be highlighted - // ex. "Hello @aris.kotsomitopoulos" + /** + * The is at least one new message that should be highlighted. + * ex. "Hello @aris.kotsomitopoulos" + */ NEW_HIGHLIGHTED_MESSAGE; } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt index edb49f4797..0c224ff17c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt @@ -49,7 +49,7 @@ interface WidgetPostAPIMediator { /** * Send a boolean response. * - * @param response the response + * @param response the response * @param eventData the modular data */ fun sendBoolResponse(response: Boolean, eventData: JsonDict) @@ -57,7 +57,7 @@ interface WidgetPostAPIMediator { /** * Send an integer response. * - * @param response the response + * @param response the response * @param eventData the modular data */ fun sendIntegerResponse(response: Int, eventData: JsonDict) @@ -65,8 +65,9 @@ interface WidgetPostAPIMediator { /** * Send an object response. * - * @param klass the class of the response - * @param response the response + * @param T the generic type + * @param type the type of the response + * @param response the response * @param eventData the modular data */ fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) @@ -81,7 +82,7 @@ interface WidgetPostAPIMediator { /** * Send an error. * - * @param message the error message + * @param message the error message * @param eventData the modular data */ fun sendError(message: String, eventData: JsonDict) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt index b06f8f7bc6..8ad6500d25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -111,8 +111,8 @@ interface WidgetService { /** * Deactivate a widget in a room. It makes sure you have the rights to handle this. * - * @param roomId: the room where you want to deactivate the widget. - * @param widgetId: the widget to deactivate. + * @param roomId the room where you want to deactivate the widget. + * @param widgetId the widget to deactivate. */ suspend fun destroyRoomWidget(roomId: String, widgetId: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index f1cfe3fee5..fe78ccc75a 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 @@ -20,7 +20,7 @@ import android.net.Uri import dagger.Lazy import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -323,8 +323,7 @@ internal class DefaultAuthenticationService @Inject constructor( } } - override val isRegistrationStarted: Boolean - get() = currentRegistrationWizard?.isRegistrationStarted == true + override fun isRegistrationStarted() = currentRegistrationWizard?.isRegistrationStarted() == true override fun getLoginWizard(): LoginWizard { return currentLoginWizard @@ -381,7 +380,7 @@ internal class DefaultAuthenticationService @Inject constructor( return getWellknownTask.execute( GetWellknownTask.Params( - domain = matrixId.getDomain(), + domain = matrixId.getServerName().substringBeforeLast(":"), homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults() ) ) 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 8f00f3440c..345f4097db 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 @@ -49,20 +49,18 @@ internal class DefaultRegistrationWizard( private val validateCodeTask: ValidateCodeTask = DefaultValidateCodeTask(authAPI) private val registerCustomTask: RegisterCustomTask = DefaultRegisterCustomTask(authAPI) - override val currentThreePid: String? - get() { - return when (val threePid = pendingSessionData.currentThreePidData?.threePid) { - is RegisterThreePid.Email -> threePid.email - is RegisterThreePid.Msisdn -> { - // Take formatted msisdn if provided by the server - pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn - } - null -> null + override fun getCurrentThreePid(): String? { + return when (val threePid = pendingSessionData.currentThreePidData?.threePid) { + is RegisterThreePid.Email -> threePid.email + is RegisterThreePid.Msisdn -> { + // Take formatted msisdn if provided by the server + pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn } + null -> null } + } - override val isRegistrationStarted: Boolean - get() = pendingSessionData.isRegistrationStarted + override fun isRegistrationStarted() = pendingSessionData.isRegistrationStarted override suspend fun getRegistrationFlow(): RegistrationResult { val params = RegistrationParams() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt index b3e9eab988..eee1ee70aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -46,6 +46,7 @@ internal class CryptoSessionInfoProvider @Inject constructor( } /** + * @param roomId the room Id * @param allActive if true return joined as well as invited, if false, only joined */ fun getRoomUserIds(roomId: String, allActive: Boolean): List { 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 11fa93dbe0..824478f1d3 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 @@ -508,7 +508,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Provides the device information for a user id and a device Id. * - * @param userId the user id + * @param userId the user id * @param deviceId the device id */ override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { @@ -538,7 +538,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Set the devices as known. * - * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. + * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. * @param callback the asynchronous callback */ override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { @@ -576,8 +576,8 @@ internal class DefaultCryptoService @Inject constructor( * Update the blocked/verified state of the given device. * * @param trustLevel the new trust level - * @param userId the owner of the device - * @param deviceId the unique identifier for the device. + * @param userId the owner of the device + * @param deviceId the unique identifier for the device. */ override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { setDeviceVerificationAction.handle(trustLevel, userId, deviceId) @@ -586,10 +586,10 @@ internal class DefaultCryptoService @Inject constructor( /** * Configure a room to use encryption. * - * @param roomId the room id to enable encryption in. - * @param algorithm the encryption config for the room. + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) - * @param membersId list of members to start tracking their devices + * @param membersId list of members to start tracking their devices * @return true if the operation succeeds. */ private suspend fun setEncryptionInRoom(roomId: String, @@ -687,9 +687,9 @@ internal class DefaultCryptoService @Inject constructor( * Encrypt an event content according to the configuration of the room. * * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param roomId the room identifier the event will be sent. - * @param callback the asynchronous callback + * @param eventType the type of the event. + * @param roomId the room identifier the event will be sent. + * @param callback the asynchronous callback */ override fun encryptEventContent(eventContent: Content, eventType: String, @@ -742,7 +742,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Decrypt an event. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the MXEventDecryptionResult data, or throw in case of error */ @@ -754,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Decrypt an event asynchronously. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param callback the callback to return data or null */ @@ -765,7 +765,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Decrypt an event. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the MXEventDecryptionResult data, or null in case of error */ @@ -905,6 +905,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Handle an m.room.encryption event. * + * @param roomId the room Id * @param event the encryption event. */ private fun onRoomEncryptionEvent(roomId: String, event: Event) { @@ -928,6 +929,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Handle a change in the membership state of a member of a room. * + * @param roomId the room Id * @param event the membership event causing the change */ private fun onRoomMembershipEvent(roomId: String, event: Event) { @@ -996,7 +998,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Export the crypto keys. * - * @param password the password + * @param password the password * @param anIterationCount the encryption iteration count (0 means no encryption) */ private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { @@ -1015,8 +1017,8 @@ internal class DefaultCryptoService @Inject constructor( /** * Import the room keys. * - * @param roomKeysAsArray the room keys as array. - * @param password the password + * @param roomKeysAsArray the room keys as array. + * @param password the password * @param progressListener the progress listener * @return the result ImportRoomKeysResult */ @@ -1066,7 +1068,7 @@ internal class DefaultCryptoService @Inject constructor( * A success means there is no unknown devices. * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered. * - * @param userIds the user ids list + * @param userIds the user ids list * @param callback the asynchronous callback. */ fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { @@ -1089,7 +1091,7 @@ internal class DefaultCryptoService @Inject constructor( * If false, it can still be overridden per-room. * If true, it overrides the per-room settings. * - * @param block true to unilaterally blacklist all + * @param block true to unilaterally blacklist all */ override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { cryptoStore.setGlobalBlacklistUnverifiedDevices(block) @@ -1129,8 +1131,8 @@ internal class DefaultCryptoService @Inject constructor( /** * Manages the room black-listing for unverified devices. * - * @param roomId the room id - * @param add true to add the room id to the list, false to remove it. + * @param roomId the room id + * @param add true to add the room id to the list, false to remove it. */ private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() @@ -1149,7 +1151,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Add this room to the ones which don't encrypt messages to unverified devices. * - * @param roomId the room id + * @param roomId the room id */ override fun setRoomBlacklistUnverifiedDevices(roomId: String) { setRoomBlacklistUnverifiedDevices(roomId, true) @@ -1158,7 +1160,7 @@ internal class DefaultCryptoService @Inject constructor( /** * Remove this room to the ones which don't encrypt messages to unverified devices. * - * @param roomId the room id + * @param roomId the room id */ override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { setRoomBlacklistUnverifiedDevices(roomId, false) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index cd4e2a6d52..18b815b3d8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -170,7 +170,7 @@ internal class DeviceListManager @Inject constructor( * Update the devices list statuses. * * @param changed the user ids list which have new devices - * @param left the user ids list which left a room + * @param left the user ids list which left a room */ fun handleDeviceListsChanges(changed: Collection, left: Collection) { Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}") @@ -223,7 +223,7 @@ internal class DeviceListManager @Inject constructor( /** * The keys download succeeded. * - * @param userIds the userIds list + * @param userIds the userIds list * @param failures the failure map. */ private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { @@ -276,7 +276,7 @@ internal class DeviceListManager @Inject constructor( * Download the device keys for a list of users and stores the keys in the MXStore. * It must be called in getEncryptingThreadHandler() thread. * - * @param userIds The users to fetch. + * @param userIds The users to fetch. * @param forceDownload Always download the keys even if cached. */ suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { @@ -421,9 +421,9 @@ internal class DeviceListManager @Inject constructor( * Validate device keys. * This method must called on getEncryptingThreadHandler() thread. * - * @param deviceKeys the device keys to validate. - * @param userId the id of the user of the device. - * @param deviceId the id of the device. + * @param deviceKeys the device keys to validate. + * @param userId the id of the user of the device. + * @param deviceId the id of the device. * @param previouslyStoredDeviceKeys the device keys we received before for this device * @return true if succeeds */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index d6f881211c..c1d04eb22b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent +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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter @@ -42,7 +43,7 @@ import javax.inject.Inject private const val SEND_TO_DEVICE_RETRY_COUNT = 3 -private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) +private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO) @SessionScope internal class EventDecryptor @Inject constructor( @@ -72,7 +73,7 @@ internal class EventDecryptor @Inject constructor( /** * Decrypt an event. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the MXEventDecryptionResult data, or throw in case of error */ @@ -84,7 +85,7 @@ internal class EventDecryptor @Inject constructor( /** * Decrypt an event asynchronously. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param callback the callback to return data or null */ @@ -100,7 +101,7 @@ internal class EventDecryptor @Inject constructor( /** * Decrypt an event. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the MXEventDecryptionResult data, or null in case of error */ @@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor( if (eventContent == null) { Timber.tag(loggerTag.value).e("decryptEvent : empty event content") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } else if (event.isRedacted()) { + // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm + return MXEventDecryptionResult( + clearEvent = mapOf( + "room_id" to event.roomId.orEmpty(), + "type" to EventType.MESSAGE, + "content" to emptyMap(), + "unsigned" to event.unsignedData.toContent() + ) + ) } else { val algorithm = eventContent["algorithm"]?.toString() val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt index e4a0f0376e..e0d6c25d70 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -68,7 +68,7 @@ internal object MXMegolmExportEncryption { /** * Decrypt a megolm key file. * - * @param data the data to decrypt + * @param data the data to decrypt * @param password the password. * @return the decrypted output. * @throws Exception the failure reason @@ -138,9 +138,9 @@ internal object MXMegolmExportEncryption { /** * Encrypt a string into the megolm export format. * - * @param data the data to encrypt. - * @param password the password - * @param kdf_rounds the iteration count + * @param data the data to encrypt. + * @param password the password + * @param kdfRounds the iteration count * @return the encrypted data * @throws Exception the failure reason */ @@ -304,9 +304,9 @@ internal object MXMegolmExportEncryption { /** * Derive the AES and HMAC-SHA-256 keys for the file. * - * @param salt salt for pbkdf + * @param salt salt for pbkdf * @param iterations number of pbkdf iterations - * @param password password + * @param password password * @return the derived keys */ @Throws(Exception::class) 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 5620cbf769..1d25d82549 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 @@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor( // So, store these message indexes per timeline id. // // The first level keys are timeline ids. - // The second level keys are strings of form "||" - private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() + // The second level values is a Map that represents: + // "|||" --> eventId + private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() init { // Retrieve the account from the store @@ -260,7 +261,7 @@ internal class MXOlmDevice @Inject constructor( * The new session will be stored in the MXStore. * * @param theirIdentityKey the remote user's Curve25519 identity key - * @param theirOneTimeKey the remote user's one-time Curve25519 key + * @param theirOneTimeKey the remote user's one-time Curve25519 key * @return the session id for the outbound session. */ fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { @@ -299,8 +300,8 @@ internal class MXOlmDevice @Inject constructor( * Generate a new inbound session, given an incoming message. * * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. - * @param messageType the message_type field from the received message (must be 0). - * @param ciphertext base64-encoded body from the received message. + * @param messageType the message_type field from the received message (must be 0). + * @param ciphertext base64-encoded body from the received message. * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. */ fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { @@ -394,8 +395,8 @@ internal class MXOlmDevice @Inject constructor( * Encrypt an outgoing message using an existing session. * * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session - * @param payloadString the payload to be encrypted and sent + * @param sessionId the id of the active session + * @param payloadString the payload to be encrypted and sent * @return the cipher text */ suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { @@ -427,10 +428,10 @@ internal class MXOlmDevice @Inject constructor( /** * Decrypt an incoming message using an existing session. * - * @param ciphertext the base64-encoded body from the received message. - * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. + * @param messageType message_type field from the received message. + * @param sessionId the id of the active session. * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. * @return the decrypted payload. */ @kotlin.jvm.Throws @@ -460,9 +461,9 @@ internal class MXOlmDevice @Inject constructor( * Determine if an incoming messages is a prekey message matching an existing session. * * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. - * @param messageType message_type field from the received message. - * @param ciphertext the base64-encoded body from the received message. + * @param sessionId the id of the active session. + * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. * @return YES if the received message is a prekey message which matchesthe given session. */ fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { @@ -563,7 +564,7 @@ internal class MXOlmDevice @Inject constructor( /** * Encrypt an outgoing message with an outbound group session. * - * @param sessionId the id of the outbound group session. + * @param sessionId the id of the outbound group session. * @param payloadString the payload to be encrypted and sent. * @return ciphertext */ @@ -590,13 +591,13 @@ internal class MXOlmDevice @Inject constructor( /** * Add an inbound group session to the session store. * - * @param sessionId the session identifier. - * @param sessionKey base64-encoded secret key. - * @param roomId the id of the room in which this session will be used. - * @param senderKey the base64-encoded curve25519 key of the sender. + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + * @param roomId the id of the room in which this session will be used. + * @param senderKey the base64-encoded curve25519 key of the sender. * @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 keysClaimed Other keys the sender claims. + * @param exportFormat true if the megolm keys are in export format * @return true if the operation succeeds. */ fun addInboundGroupSession(sessionId: String, @@ -752,70 +753,75 @@ internal class MXOlmDevice @Inject constructor( /** * Decrypt a received message with an inbound group session. * - * @param body the base64-encoded body of the encrypted message. - * @param roomId the room in which the message was received. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param body the base64-encoded body of the encrypted message. + * @param roomId the room in which the message was received. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param eventId the eventId of the message that will be decrypted * @param sessionId the session identifier. * @param senderKey the base64-encoded curve25519 key of the sender. - * @return the decrypting result. Nil if the sessionId is unknown. + * @return the decrypting result. Null if the sessionId is unknown. */ @Throws(MXCryptoError::class) suspend fun decryptGroupMessage(body: String, roomId: String, timeline: String?, + eventId: String, sessionId: String, senderKey: String): 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") - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId == wrapper.roomId) { - val decryptResult = try { - sessionHolder.mutex.withLock { - inboundGroupSession.decryptMessage(body) - } - } catch (e: OlmException) { - Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") - throw MXCryptoError.OlmError(e) - } - - if (timeline?.isNotBlank() == true) { - val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } - - val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex - - if (timelineSet.contains(messageIndexKey)) { - val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) - } - - timelineSet.add(messageIndexKey) - } - - inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) - val payload = try { - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) - adapter.fromJson(payloadString) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - return OlmDecryptionResult( - payload, - wrapper.keysClaimed, - senderKey, - wrapper.forwardingCurve25519KeyChain - ) - } else { + 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. val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) } + val decryptResult = try { + sessionHolder.mutex.withLock { + inboundGroupSession.decryptMessage(body) + } + } catch (e: OlmException) { + Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") + throw MXCryptoError.OlmError(e) + } + + val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex + Timber.tag(loggerTag.value).v("##########################################################") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId") + Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}") + + if (timeline?.isNotBlank() == true) { + val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() } + if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) + } + replayAttackMap[messageIndexKey] = eventId + } + inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) + val payload = try { + val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) + val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) + adapter.fromJson(payloadString) + } catch (e: Exception) { + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + return OlmDecryptionResult( + payload, + wrapper.keysClaimed, + senderKey, + wrapper.forwardingCurve25519KeyChain + ) } /** @@ -834,9 +840,9 @@ internal class MXOlmDevice @Inject constructor( /** * Verify an ed25519 signature on a JSON object. * - * @param key the ed25519 key. + * @param key the ed25519 key. * @param jsonDictionary the JSON object which was signed. - * @param signature the base64-encoded signature to be checked. + * @param signature the base64-encoded signature to be checked. * @throws Exception the exception */ @Throws(Exception::class) @@ -859,7 +865,7 @@ internal class MXOlmDevice @Inject constructor( * Search an OlmSession. * * @param theirDeviceIdentityKey the device key - * @param sessionId the session Id + * @param sessionId the session Id * @return the olm session */ private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { @@ -873,9 +879,9 @@ internal class MXOlmDevice @Inject constructor( * Extract an InboundGroupSession from the session store and do some check. * inboundGroupSessionWithIdError describes the failure reason. * - * @param roomId the room where the session is used. * @param sessionId the session identifier. * @param senderKey the base64-encoded curve25519 key of the sender. + * @param roomId the room where the session is used. * @return the inbound group session. */ fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { @@ -905,7 +911,7 @@ internal class MXOlmDevice @Inject constructor( /** * Determine if we have the keys for a given megolm session. * - * @param roomId room in which the message was received + * @param roomId room in which the message was received * @param senderKey base64-encoded curve25519 key of the sender * @param sessionId session identifier * @return true if the unbound session keys are known. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt index fe280416ea..4401a07192 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt @@ -39,7 +39,7 @@ internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoS * Store a session between our own device and another device. * This will be called after the session has been created but also every time it has been used * in order to persist the correct state for next run - * @param olmSessionWrapper the end-to-end session. + * @param olmSessionWrapper the end-to-end session. * @param deviceKey the public key of the other device. */ @Synchronized diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt index c2f494b4b3..a80bafbe79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt @@ -49,7 +49,7 @@ internal class RoomDecryptorProvider @Inject constructor( * If we already have a decryptor for the given room and algorithm, return * it. Otherwise try to instantiate it. * - * @param roomId the room id + * @param roomId the room id * @param algorithm the crypto algorithm * @return the decryptor * // TODO Create another method for the case of roomId is null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index fc211537a6..4c5720daf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -29,7 +29,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o /** * Try to make sure we have established olm sessions for the given users. - * @param users a list of user ids. + * @param users a list of user ids. */ suspend fun handle(users: List): MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") 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 22c4e59b18..67d73c21ed 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 @@ -45,8 +45,8 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi * Must be call on the crypto coroutine thread * * @param megolmSessionsData megolm sessions. - * @param fromBackup true if the imported keys are already backed up on the server. - * @param progressListener the progress listener + * @param fromBackup true if the imported keys are already backed up on the server. + * @param progressListener the progress listener * @return import room keys result */ @WorkerThread diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt index 9bbbab4992..919e38c391 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt @@ -42,7 +42,7 @@ internal class MessageEncrypter @Inject constructor( * This method must be called from the getCryptoHandler() thread. * * @param payloadFields fields to include in the encrypted payload. - * @param deviceInfos list of device infos to encrypt for. + * @param deviceInfos list of device infos to encrypt for. * @return the content for an m.room.encrypted event. */ suspend fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 34006ecfde..6847a46369 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -29,7 +29,7 @@ internal interface IMXDecrypting { /** * Decrypt an event. * - * @param event the raw event. + * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the decryption information, or an error */ @@ -40,6 +40,7 @@ internal interface IMXDecrypting { * Handle a key event. * * @param event the key event. + * @param defaultKeysBackupService the keys backup service */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} } 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 1d84120208..73ce5a5004 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 @@ -27,8 +27,8 @@ internal interface IMXEncrypting { * Encrypt an event content according to the configuration of the room. * * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param userIds the room members the event will be sent to. + * @param eventType the type of the event. + * @param userIds the room members the event will be sent to. * @return the encrypted content */ suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt index 6f488def0a..8cf01f1972 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt @@ -38,7 +38,7 @@ internal interface IMXGroupEncryption { * Re-shares a session key with devices if the key has already been * sent to them. * - * @param sessionId The id of the outbound session to share. + * @param groupSessionId The id of the outbound session to share. * @param userId The id of the user who owns the target device. * @param deviceId The id of the target device. * @param senderKey The key of the originating device for the session. 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 8321b73b75..722462bf0e 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 @@ -78,6 +78,7 @@ internal class MXMegolmDecryption( encryptedEventContent.ciphertext, event.roomId, timeline, + eventId = event.eventId.orEmpty(), encryptedEventContent.sessionId, encryptedEventContent.senderKey ) @@ -176,6 +177,7 @@ internal class MXMegolmDecryption( * Handle a key event. * * @param event the key event. + * @param defaultKeysBackupService the keys backup service */ override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { Timber.tag(loggerTag.value).v("onRoomKeyEvent()") 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 79e907945f..8b4e9df607 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 @@ -198,7 +198,7 @@ internal class MXMegolmEncryption( /** * Share the device key to a list of users. * - * @param session the session info + * @param session the session info * @param devicesByUsers the devices map */ private suspend fun shareKey(session: MXOutboundSessionInfo, @@ -227,7 +227,7 @@ internal class MXMegolmEncryption( /** * Share the device keys of a an user. * - * @param session the session info + * @param session the session info * @param devicesByUser the devices map */ private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, @@ -387,7 +387,7 @@ internal class MXMegolmEncryption( * Get the list of devices which can encrypt data to. * This method must be called in getDecryptingThreadHandler() thread. * - * @param userIds the user ids whose devices must be checked. + * @param userIds the user ids whose devices must be checked. */ private suspend fun getDevicesInRoom(userIds: List): DeviceInRoomInfo { // We are happy to use a cached version here: we assume that if we already diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 1e66fe84c9..23c8f0e905 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -180,8 +180,8 @@ internal class MXOlmDecryption( /** * Attempt to decrypt an Olm message. * + * @param message message object, with 'type' and 'body' fields. * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. - * @param message message object, with 'type' and 'body' fields. * @return payload, if decrypted successfully. */ private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt index 3c9706abe1..bde1d65093 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -70,7 +70,7 @@ internal class MXOlmEncryption( /** * Ensure that the session. * - * @param users the user ids list + * @param users the user ids list */ private suspend fun ensureSession(users: List) { deviceListManager.downloadKeys(users, false) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index f21f5e05e1..f5ead35933 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -103,7 +103,7 @@ internal interface CryptoApi { * Claim one-time keys. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim * - * @param params the params. + * @param body the Json body. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") suspend fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): KeysClaimResponse @@ -112,9 +112,9 @@ internal interface CryptoApi { * Send an event to a specific list of devices * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-sendtodevice-eventtype-txnid * - * @param eventType the type of event to send + * @param eventType the type of event to send * @param transactionId the transaction ID for this event - * @param body the body + * @param body the body */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}") suspend fun sendToDevice(@Path("eventType") eventType: String, @@ -126,7 +126,7 @@ internal interface CryptoApi { * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-devices-deviceid * * @param deviceId the device id - * @param params the deletion parameters + * @param params the deletion parameters */ @HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true) suspend fun deleteDevice(@Path("device_id") deviceId: String, @@ -137,7 +137,7 @@ internal interface CryptoApi { * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid * * @param deviceId the device id - * @param params the params + * @param params the params */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}") suspend fun updateDeviceInfo(@Path("device_id") deviceId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index b4cbd15109..7ff08cd127 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -159,6 +159,7 @@ internal object MXEncryptedAttachments { * Encrypt an attachment stream. * DO NOT USE for big files, it will load all in memory * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param clock a clock to retrieve current time * @return the encryption file info */ fun encryptAttachment(attachmentStream: InputStream, clock: Clock): EncryptionResult { @@ -231,7 +232,8 @@ internal object MXEncryptedAttachments { * * @param attachmentStream the attachment stream. Will be closed after this method call. * @param elementToDecrypt the elementToDecrypt info - * @param outputStream the outputStream where the decrypted attachment will be write. + * @param outputStream the outputStream where the decrypted attachment will be write. + * @param clock a clock to retrieve current time * @return true in case of success, false in case of error */ fun decryptAttachment(attachmentStream: InputStream?, 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 5ea4695da2..efd3bca824 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 @@ -137,17 +137,11 @@ internal class DefaultKeysBackupService @Inject constructor( private var keysBackupStateListener: KeysBackupStateListener? = null - override val isEnabled: Boolean - get() = keysBackupStateManager.isEnabled + override fun isEnabled(): Boolean = keysBackupStateManager.isEnabled - override val isStucked: Boolean - get() = keysBackupStateManager.isStucked + override fun isStuck(): Boolean = keysBackupStateManager.isStuck - override val state: KeysBackupState - get() = keysBackupStateManager.state - - override val currentBackupVersion: String? - get() = keysBackupVersion?.version + override fun getState(): KeysBackupState = keysBackupStateManager.state override fun addListener(listener: KeysBackupStateListener) { keysBackupStateManager.addListener(listener) @@ -291,7 +285,7 @@ internal class DefaultKeysBackupService @Inject constructor( this.callback = object : MatrixCallback { private fun eventuallyRestartBackup() { // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (state == KeysBackupState.Unknown) { + if (getState() == KeysBackupState.Unknown) { checkAndStartKeysBackup() } } @@ -347,7 +341,7 @@ internal class DefaultKeysBackupService @Inject constructor( override fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback?) { - if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) { + if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { callback?.onFailure(Throwable("Backup not enabled")) return } @@ -383,7 +377,7 @@ internal class DefaultKeysBackupService @Inject constructor( } // If backup is finished, notify the main listener - if (state === KeysBackupState.ReadyToBackUp) { + if (getState() === KeysBackupState.ReadyToBackUp) { backupAllGroupSessionsCallback?.onSuccess(Unit) resetBackupAllGroupSessionsListeners() } @@ -628,7 +622,7 @@ internal class DefaultKeysBackupService @Inject constructor( } } - override fun onSecretKeyGossip(secret: String) { + fun onSecretKeyGossip(secret: String) { Timber.i("## CrossSigning - onSecretKeyGossip") cryptoCoroutineScope.launch(coroutineDispatchers.io) { @@ -915,12 +909,12 @@ internal class DefaultKeysBackupService @Inject constructor( */ fun maybeBackupKeys() { when { - isStucked -> { + isStuck() -> { // If not already done, or in error case, check for a valid backup version on the homeserver. // If there is one, maybeBackupKeys will be called again. checkAndStartKeysBackup() } - state == KeysBackupState.ReadyToBackUp -> { + getState() == KeysBackupState.ReadyToBackUp -> { keysBackupStateManager.state = KeysBackupState.WillBackUp // Wait between 0 and 10 seconds, to avoid backup requests from @@ -933,8 +927,8 @@ internal class DefaultKeysBackupService @Inject constructor( uiHandler.post { backupKeys() } } } - else -> { - Timber.v("maybeBackupKeys: Skip it because state: $state") + else -> { + Timber.v("maybeBackupKeys: Skip it because state: ${getState()}") } } } @@ -1018,9 +1012,9 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun checkAndStartKeysBackup() { - if (!isStucked) { + if (!isStuck()) { // Try to start or restart the backup only if it is in unknown or bad state - Timber.w("checkAndStartKeysBackup: invalid state: $state") + Timber.w("checkAndStartKeysBackup: invalid state: ${getState()}") return } @@ -1105,6 +1099,7 @@ internal class DefaultKeysBackupService @Inject constructor( * * @param password the password. * @param keysBackupData the backup and its auth data. + * @param progressListener listener to track progress * * @return the recovery key if successful, null in other cases */ @@ -1258,16 +1253,16 @@ internal class DefaultKeysBackupService @Inject constructor( Timber.v("backupKeys") // Sanity check, as this method can be called after a delay, the state may have change during the delay - if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) { + if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { Timber.v("backupKeys: Invalid configuration") backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) resetBackupAllGroupSessionsListeners() return } - if (state === KeysBackupState.BackingUp) { + if (getState() === KeysBackupState.BackingUp) { // Do nothing if we are already backing up - Timber.v("backupKeys: Invalid state: $state") + Timber.v("backupKeys: Invalid state: ${getState()}") return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt index d5bab33180..f821fdcf6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -44,6 +44,7 @@ internal data class GeneratePrivateKeyResult( * Compute a private key from a password. * * @param password the password to use. + * @param progressListener a listener to track progress * * @return a {privateKey, salt, iterations} tuple. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt index 78ef958bbf..0614eceb16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -49,7 +49,7 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) { state == KeysBackupState.BackingUp // True if unknown or bad state - val isStucked: Boolean + val isStuck: Boolean get() = state == KeysBackupState.Unknown || state == KeysBackupState.Disabled || state == KeysBackupState.WrongBackUpVersion || diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt index ea23be5923..d9c63b46ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -60,19 +60,19 @@ internal interface RoomKeysApi { * Get information about the given version. * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} * - * @param version version + * @param version version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") suspend fun getKeysBackupVersion(@Path("version") version: String): KeysVersionResult /** * Update information about the given version. - * @param version version + * @param version version * @param updateKeysBackupVersionBody the body */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") suspend fun updateKeysBackupVersion(@Path("version") version: String, - @Body keysBackupVersionBody: UpdateKeysBackupVersionBody) + @Body updateKeysBackupVersionBody: UpdateKeysBackupVersionBody) /* ========================================================================================== * Storing keys @@ -87,9 +87,9 @@ internal interface RoomKeysApi { * flag (true is better than false), then by the first_message_index (a lower number is better), and finally by * forwarded_count (a lower number is better). * - * @param roomId the room id - * @param sessionId the session id - * @param version the version of the backup + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup * @param keyBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") @@ -101,8 +101,8 @@ internal interface RoomKeysApi { /** * Store several keys for the given room, using the given backup version. * - * @param roomId the room id - * @param version the version of the backup + * @param roomId the room id + * @param version the version of the backup * @param roomKeysBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") @@ -113,7 +113,7 @@ internal interface RoomKeysApi { /** * Store several keys, using the given backup version. * - * @param version the version of the backup + * @param version the version of the backup * @param keysBackupData the data to send */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") @@ -127,9 +127,9 @@ internal interface RoomKeysApi { /** * Retrieve the key for the given session in the given room from the backup. * - * @param roomId the room id + * @param roomId the room id * @param sessionId the session id - * @param version the version of the backup, or empty String to retrieve the last version + * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") suspend fun getRoomSessionData(@Path("roomId") roomId: String, @@ -139,8 +139,8 @@ internal interface RoomKeysApi { /** * Retrieve all the keys for the given room from the backup. * - * @param roomId the room id - * @param version the version of the backup, or empty String to retrieve the last version + * @param roomId the room id + * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") suspend fun getRoomSessionsData(@Path("roomId") roomId: String, @@ -149,7 +149,7 @@ internal interface RoomKeysApi { /** * Retrieve all the keys from the backup. * - * @param version the version of the backup, or empty String to retrieve the last version + * @param version the version of the backup, or empty String to retrieve the last version */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") suspend fun getSessionsData(@Query("version") version: String): KeysBackupData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt index 6bfa56ae8d..6b747d19f2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt @@ -59,7 +59,7 @@ internal data class MXKey( /** * Returns a signature for an user Id and a signkey. * - * @param userId the user id + * @param userId the user id * @param signkey the sign key * @return the signature */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 1ded9c6c7e..0daaec11a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfo import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.KeySigner import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent @@ -157,7 +158,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return getKey(keyId) } - override suspend fun storeSecret(name: String, secretBase64: String, keys: List) { + override suspend fun storeSecret(name: String, secretBase64: String, keys: List) { withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.computation) { val encryptedContents = HashMap() keys.forEach { 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 480009dbce..9b1c785059 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 @@ -164,16 +164,14 @@ internal interface IMXCryptoStore { /** * Store the end to end account for the logged-in user. - * - * @param account the account to save */ fun saveOlmAccount() /** * Retrieve a device for a user. * + * @param userId the user's id. * @param deviceId the device id. - * @param userId the user's id. * @return the device */ fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? @@ -189,7 +187,7 @@ internal interface IMXCryptoStore { /** * Store the known devices for a user. * - * @param userId The user's id. + * @param userId The user's id. * @param devices A map from device id to 'MXDevice' object for the device. */ fun storeUserDevices(userId: String, devices: Map?) @@ -225,7 +223,7 @@ internal interface IMXCryptoStore { /** * Store the crypto algorithm for a room. * - * @param roomId the id of the room. + * @param roomId the id of the room. * @param algorithm the algorithm. */ fun storeRoomAlgorithm(roomId: String, algorithm: String?) @@ -253,7 +251,7 @@ internal interface IMXCryptoStore { /** * Store a session between the logged-in user and another device. * - * @param olmSessionWrapper the end-to-end session. + * @param olmSessionWrapper the end-to-end session. * @param deviceKey the public key of the other device. */ fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) @@ -331,7 +329,7 @@ internal interface IMXCryptoStore { /** * Mark inbound group sessions as backed up on the user homeserver. * - * @param sessions the sessions + * @param olmInboundGroupSessionWrappers the sessions */ fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) @@ -361,7 +359,7 @@ internal interface IMXCryptoStore { /** * Get the tracking status of a specified userId devices. * - * @param userId the user id + * @param userId the user id * @param defaultValue the default value * @return the tracking status */ @@ -380,7 +378,9 @@ internal interface IMXCryptoStore { /** * Look for an existing outgoing room key request, and if none is found, add a new one. * - * @param request the request + * @param requestBody the request + * @param recipients list of recipients + * @param fromIndex start index * @return either the same instance as passed in, or the existing one. */ fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt index 4ab7e0e30c..04fb6c4858 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -35,7 +35,7 @@ internal object HkdfSha256 { /** * HkdfSha256-Extract(salt, IKM) -> PRK. * - * @param salt optional salt value (a non-secret random value); + * @param salt optional salt value (a non-secret random value); * if not provided, it is set to a string of HashLen (size in octets) zeros. * @param ikm input keying material */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 6da674d6e4..af48283767 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -939,9 +939,25 @@ internal class DefaultVerificationService @Inject constructor( updatePendingRequest( existingRequest.copy( - readyInfo = readyReq + readyInfo = readyReq ) ) + + notifyOthersOfAcceptance(readyReq.transactionId, readyReq.fromDevice) + } + + /** + * Gets a list of device ids excluding the current one. + */ + private fun getMyOtherDeviceIds(): List = cryptoStore.getUserDevices(userId)?.keys?.filter { it != deviceId }.orEmpty() + + /** + * Notifies other devices that the current verification transaction is being handled by [acceptedByDeviceId]. + */ + private fun notifyOthersOfAcceptance(transactionId: String, acceptedByDeviceId: String) { + val deviceIds = getMyOtherDeviceIds().filter { it != acceptedByDeviceId } + val transport = verificationTransportToDeviceFactory.createTransport(null) + transport.cancelTransaction(transactionId, userId, deviceIds, CancelCode.AcceptedByAnotherDevice) } private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt index c12aea9d52..69dec12ef3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt @@ -35,6 +35,11 @@ internal interface VerificationTransport { onDone: (() -> Unit)?) /** + * @param supportedMethods list of supported method by this client + * @param localId a local Id + * @param otherUserId the user id to send the verification request to + * @param roomId a room Id to use to send verification message + * @param toDevices list of device Ids * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success */ fun sendVerificationRequest(supportedMethods: List, @@ -49,6 +54,11 @@ internal interface VerificationTransport { otherUserDeviceId: String?, code: CancelCode) + fun cancelTransaction(transactionId: String, + otherUserId: String, + otherUserDeviceIds: List, + code: CancelCode) + fun done(transactionId: String, onDone: (() -> Unit)?) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index e32828af23..03df849d22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -160,6 +160,9 @@ internal class VerificationTransportRoomMessage( } } + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) = + cancelTransaction(transactionId, otherUserId, null, code) + override fun done(transactionId: String, onDone: (() -> Unit)?) { Timber.d("## SAS sending done for $transactionId") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt index bc24ef2966..974adf3888 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -193,6 +193,27 @@ internal class VerificationTransportToDevice( .executeBy(taskExecutor) } + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap() + val messages = otherUserDeviceIds.associateWith { cancelMessage } + contentMap.setObjects(otherUserId, messages) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + } + } + .executeBy(taskExecutor) + } + override fun createAccept(tid: String, keyAgreementProtocol: String, hash: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 55bccfd1ec..592461f927 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 28L + val schemaVersion = 29L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -93,5 +94,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 26) MigrateSessionTo026(realm).perform() if (oldVersion < 27) MigrateSessionTo027(realm).perform() if (oldVersion < 28) MigrateSessionTo028(realm).perform() + if (oldVersion < 29) MigrateSessionTo029(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index db3647c3fa..5db859bce2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -96,7 +96,9 @@ internal fun EventEntity.markEventAsRoot( /** * Count the number of threads for the provided root thread eventId, and finds the latest event message. * Note: Redactions are handled by RedactionEventProcessor. + * @param realm the realm database * @param rootThreadEventId The root eventId that will find the number of threads + * @param chunkEntity the chunk entity * @return A ThreadSummary containing the counted threads and the latest event message */ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { @@ -184,6 +186,7 @@ private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: St /** * Find all TimelineEventEntity that are root threads for the specified room. + * @param realm the realm instance * @param roomId The room that all stored root threads will be returned */ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery = @@ -218,6 +221,7 @@ internal fun List.mapEventsWithEdition(realm: Realm, roomId: Stri /** * Returns a list of all the marked unread threads that exists for the specified room. + * @param realm the realm instance * @param roomId The roomId that the user is currently in */ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery = @@ -232,6 +236,7 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo /** * Returns whether or not the given user is participating in a current thread. + * @param realm the realm instance * @param roomId the room that the thread exists * @param rootThreadEventId the thread that the search will be done * @param senderId the user that will try to find participation @@ -247,6 +252,7 @@ internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Re /** * Returns whether or not the given user is mentioned in a current thread. + * @param realm the realm instance * @param roomId the room that the thread exists * @param rootThreadEventId the thread that the search will be done * @param userId the user that will try to find if there is a mention diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 3bf574c207..5b4fe287cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -303,6 +303,7 @@ private fun getLatestEvent(rootThreadEvent: Event): Event? { /** * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server. * note: Sorting cannot be provided by server, so we have to use that unstable property. + * @param realm the realm instance * @param roomId The id of the room */ internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery = diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index c747ad334f..6bbeb17fdd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -58,7 +58,7 @@ internal object EventAnnotationsSummaryMapper { PollResponseAggregatedSummaryEntityMapper.map(it) }, liveLocationShareAggregatedSummary = annotationsSummary.liveLocationShareAggregatedSummary?.let { - LiveLocationShareAggregatedSummaryMapper.map(it) + LiveLocationShareAggregatedSummaryMapper().map(it) } ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt index 71b36f88bd..9460e4c6ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt @@ -20,11 +20,13 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import javax.inject.Inject -internal object LiveLocationShareAggregatedSummaryMapper { +internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() { fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { return LiveLocationShareAggregatedSummary( + userId = entity.userId, isActive = entity.isActive, endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis, lastLocationDataContent = ContentMapper.map(entity.lastLocationContent).toModel() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt new file mode 100644 index 0000000000..aebca11c2b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.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.internal.database.migration + +import io.realm.DynamicRealm +import io.realm.FieldAttribute +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field userId. + */ +internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addField(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, String::class.java, FieldAttribute.REQUIRED) + ?.transform { obj -> + obj.setString(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, "") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index 4d0d2c5c64..c5df8e9338 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt @@ -31,6 +31,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity( var roomId: String = "", + var userId: String = "", + + /** + * Indicate whether the live is currently running. + */ var isActive: Boolean? = null, var endOfLiveTimestampMillis: Long? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 2e2e939fa2..0cc41413f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -28,9 +28,15 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( roomId: String, eventId: String, ): RealmQuery { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) +} + +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm, + roomId: String): RealmQuery { return realm.where() .equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId) - .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) } internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create( @@ -55,3 +61,39 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.getOrCreate( return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() ?: LiveLocationShareAggregatedSummaryEntity.create(realm, roomId, eventId) } + +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( + realm: Realm, + roomId: String, + eventId: String, +): LiveLocationShareAggregatedSummaryEntity? { + return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() +} + +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( + realm: Realm, + roomId: String, + userId: String, + ignoredEventId: String +): List { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId) + .findAll() +} + +/** + * A live is considered as running when active and with at least a last known location. + */ +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveInRoom( + realm: Realm, + roomId: String, +): RealmQuery { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) + .isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java index a1b46f6c09..b2bb852cd1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java @@ -612,7 +612,7 @@ public class HomeServerConnectionConfig { * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf * - https://developer.android.com/reference/javax/net/ssl/SSLEngine * - * @param tlsLimitations true to use Tls limitations + * @param tlsLimitations true to use Tls limitations * @param enableCompatibilityMode set to true for Android < 20 * @return this builder */ @@ -649,7 +649,7 @@ public class HomeServerConnectionConfig { /** * @param proxyHostname Proxy Hostname - * @param proxyPort Proxy Port + * @param proxyPort Proxy Port * @return this builder */ public Builder withProxy(@Nullable String proxyHostname, int proxyPort) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index 695e7525af..87a98e03f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -32,6 +32,7 @@ import java.io.IOException * Execute a request from the requestBlock and handle some of the Exception it could generate * Ref: https://github.com/matrix-org/matrix-js-sdk/blob/develop/src/scheduler.js#L138-L175 * + * @param DATA type of data return by the [requestBlock] * @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError] * @param canRetry if set to true, the request will be executed again in case of error, after a delay * @param maxDelayBeforeRetry the max delay to wait before a retry. Note that in the case of a 429, if the provided delay exceeds this value, the error will diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt index 40d174ee2d..dd41b9f6fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt @@ -119,9 +119,11 @@ internal class RuntimeJsonAdapterFactory( companion object { /** + * @param T the generic type to pass to [RuntimeJsonAdapterFactory] * @param baseType The base type for which this factory will create adapters. Cannot be Object. * @param labelKey The key in the JSON object whose value determines the type to which to map the * JSON object. + * @param fallbackType alternative Type to try in case of the serialization fails */ @CheckReturnValue fun of(baseType: Class, labelKey: String, fallbackType: Class): RuntimeJsonAdapterFactory { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt index 2ef40fe2a3..e5659fd76b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt @@ -94,6 +94,7 @@ internal object CertUtil { * Convert the fingerprint to an hexa string. * * @param fingerprint the fingerprint + * @param sep the separator character, default to space * @return the hexa string. */ fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt index ccae5ad14f..539570cdd9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt @@ -24,11 +24,9 @@ import javax.net.ssl.X509TrustManager /** * Implements a TrustManager that checks Certificates against an explicit list of known * fingerprints. - */ - -/** - * @param fingerprints Not empty array of SHA256 cert fingerprints - * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * + * @property fingerprints Not empty array of SHA256 cert fingerprints + * @property defaultTrustManager Optional trust manager to fall back on if cert does not match * any of the fingerprints. Can be null. */ internal class PinnedTrustManager(private val fingerprints: List, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt index 574f1ef81d..191bb90a67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt @@ -28,11 +28,9 @@ import javax.net.ssl.X509ExtendedTrustManager /** * Implements a TrustManager that checks Certificates against an explicit list of known * fingerprints. - */ - -/** - * @param fingerprints An array of SHA256 cert fingerprints - * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * + * @property fingerprints An array of SHA256 cert fingerprints + * @property defaultTrustManager Optional trust manager to fall back on if cert does not match * any of the fingerprints. Can be null. */ @RequiresApi(Build.VERSION_CODES.N) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 050480e6c9..1e3566f49e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.room.RoomModule +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker @@ -131,6 +132,8 @@ internal interface SessionComponent { fun inject(worker: UpdateTrustWorker) + fun inject(worker: DeactivateLiveLocationShareWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 7ceb89e892..9208ff219b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -87,8 +87,8 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor -import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DefaultLiveLocationAggregationProcessor -import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor @@ -389,5 +389,5 @@ internal abstract class SessionModule { abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor @Binds - abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor + abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt index 19b9130fc4..0db6812609 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt @@ -55,6 +55,7 @@ internal interface DirectoryAPI { /** * Add alias to the room. * @param roomAlias the room alias. + * @param body the Json body */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") suspend fun addRoomAlias(@Path("roomAlias") roomAlias: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt index dab801360f..d1df77d14a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt @@ -28,7 +28,7 @@ internal interface FilterApi { * Upload FilterBody to get a filter_id which can be used for /sync requests. * * @param userId the user id - * @param body the Json representation of a FilterBody object + * @param body the Json representation of a FilterBody object */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") suspend fun uploadFilter(@Path("userId") userId: String, @@ -37,7 +37,7 @@ internal interface FilterApi { /** * Gets a filter with a given filterId from the homeserver. * - * @param userId the user id + * @param userId the user id * @param filterId the filterID * @return Filter */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt index 562fea88b6..2017a86c39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt @@ -25,7 +25,7 @@ internal object FilterUtil { * FIXME New expected filter: * "{\"room\": {\"ephemeral\": {\"notTypes\": [\"m.typing\"]}}, \"presence\":{\"notTypes\": [\"*\"]}}" * - * @param filterBody filterBody to patch + * @param filterBody filterBody to patch * @param useDataSaveMode true to enable data save mode */ /* diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index e9097e4d03..b1a518724c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.session.homeserver import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.extensions.orFalse @@ -93,10 +93,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } }.getOrNull() + // Domain may include a port (eg, matrix.org:8080) + // Per https://spec.matrix.org/latest/client-server-api/#well-known-uri we should extract the hostname from the server name + // So we take everything before the last : as the domain for the well-known task. + // NB: This is not always the same endpoint as capabilities / mediaConfig uses. val wellknownResult = runCatching { getWellknownTask.execute( GetWellknownTask.Params( - domain = userId.getDomain(), + domain = userId.getServerName().substringBeforeLast(":"), homeServerConnectionConfig = homeServerConnectionConfig ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt index eb8c841d57..c3caaefdec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -32,6 +32,7 @@ internal interface OpenIdAPI { * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token * * @param userId the user id + * @param body an empty json body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") suspend fun openIdToken(@Path("userId") userId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index 0f667c65df..edc45fe945 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.internal.session.permalinks -import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -55,9 +55,9 @@ internal class ViaParameterFinder @Inject constructor( } fun computeViaParams(userId: String, roomId: String, max: Int): List { - val userHomeserver = userId.getDomain() + val userHomeserver = userId.getServerName() return getUserIdsOfJoinedMembers(roomId) - .map { it.getDomain() } + .map { it.getServerName() } .groupBy { it } .mapValues { it.value.size } .toMutableMap() @@ -92,7 +92,7 @@ internal class ViaParameterFinder @Inject constructor( .orEmpty() .toSet() - return userThatCanInvite.map { it.getDomain() } + return userThatCanInvite.map { it.getServerName() } .groupBy { it } .mapValues { it.value.size } .toMutableMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index 13b990a9ff..9437550d7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.pushers import androidx.lifecycle.LiveData import androidx.work.BackoffPolicy import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.HttpPusher import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -58,15 +59,15 @@ internal class DefaultPushersService @Inject constructor( .executeBy(taskExecutor) } - override fun enqueueAddHttpPusher(httpPusher: PushersService.HttpPusher): UUID { + override fun enqueueAddHttpPusher(httpPusher: HttpPusher): UUID { return enqueueAddPusher(httpPusher.toJsonPusher()) } - override suspend fun addHttpPusher(httpPusher: PushersService.HttpPusher) { + override suspend fun addHttpPusher(httpPusher: HttpPusher) { addPusherTask.execute(AddPusherTask.Params(httpPusher.toJsonPusher())) } - private fun PushersService.HttpPusher.toJsonPusher() = JsonPusher( + private fun HttpPusher.toJsonPusher() = JsonPusher( pushKey = pushkey, kind = "http", appId = appId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt index 40b4ee269a..fbae04a1f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt @@ -33,9 +33,9 @@ internal interface PushRulesApi { /** * Update the ruleID enable status. * - * @param kind the notification kind (sender, room...) + * @param kind the notification kind (sender, room...) * @param ruleId the ruleId - * @param enable the new enable status + * @param enabledBody the new enable status */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled") suspend fun updateEnableRuleStatus(@Path("kind") kind: String, @@ -46,8 +46,8 @@ internal interface PushRulesApi { * Update the ruleID action. * Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions * - * @param kind the notification kind (sender, room...) - * @param ruleId the ruleId + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId * @param actions the actions */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions") @@ -58,7 +58,7 @@ internal interface PushRulesApi { /** * Delete a rule. * - * @param kind the notification kind (sender, room...) + * @param kind the notification kind (sender, room...) * @param ruleId the ruleId */ @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") @@ -68,9 +68,9 @@ internal interface PushRulesApi { /** * Add the ruleID enable status. * - * @param kind the notification kind (sender, room...) + * @param kind the notification kind (sender, room...) * @param ruleId the ruleId. - * @param rule the rule to add. + * @param rule the rule to add. */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") suspend fun addRule(@Path("kind") kind: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 7326adee4c..abea2d34cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType @@ -69,6 +70,7 @@ internal class DefaultRoom( private val roomAccountDataService: RoomAccountDataService, private val roomVersionService: RoomVersionService, private val viaParameterFinder: ViaParameterFinder, + private val locationSharingService: LocationSharingService, override val coroutineDispatchers: MatrixCoroutineDispatchers ) : Room { @@ -104,4 +106,5 @@ internal class DefaultRoom( override fun roomPushRuleService() = roomPushRuleService override fun roomAccountDataService() = roomAccountDataService override fun roomVersionService() = roomVersionService + override fun locationSharingService() = locationSharingService } 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 8424ee8a36..a7a5dbf4f3 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 @@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.Room @@ -91,18 +90,25 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummary(roomIdOrAlias) } + override fun getRoomSummaryLive(roomId: String): LiveData> { + return roomSummaryDataSource.getRoomSummaryLive(roomId) + } + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder): List { return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder) } override fun refreshJoinedRoomSummaryPreviews(roomId: String?) { - val roomSummaries = getRoomSummaries(roomSummaryQueryParams { - if (roomId != null) { - this.roomId = QueryStringValue.Equals(roomId) - } - memberships = listOf(Membership.JOIN) - }) + val roomSummaries = if (roomId == null) { + getRoomSummaries(roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + }) + } else { + listOfNotNull( + getRoomSummary(roomId)?.takeIf { it.membership == Membership.JOIN } + ) + } if (roomSummaries.isNotEmpty()) { monarchy.runTransactionSync { realm -> @@ -127,7 +133,7 @@ internal class DefaultRoomService @Inject constructor( override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder): UpdatableLivePageResult { - return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) + return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenedParents = true) } override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 16a63a9a96..3efeef7688 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation @@ -28,23 +27,16 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.api.session.room.model.PollSummaryContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent -import org.matrix.android.sdk.api.session.room.model.VoteInfo -import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent @@ -55,7 +47,6 @@ import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity @@ -68,6 +59,7 @@ import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -79,6 +71,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( @SessionId private val sessionId: String, private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, + private val pollAggregationProcessor: PollAggregationProcessor, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -162,9 +155,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } else if (event.getClearType() in EventType.POLL_RESPONSE) { - event.getClearContent().toModel(catchError = true)?.let { pollResponseContent -> - Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) } } } @@ -184,18 +176,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } in EventType.POLL_RESPONSE -> { event.getClearContent().toModel(catchError = true)?.let { - handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) + } } } in EventType.POLL_END -> { - event.content.toModel(catchError = true)?.let { - handleEndPoll(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + getPowerLevelsHelper(event.roomId)?.let { + pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) + } } } in EventType.BEACON_LOCATION_DATA -> { - event.getClearContent().toModel(catchError = true)?.let { - liveLocationAggregationProcessor.handleBeaconLocationData(realm, event, it, roomId, isLocalEcho) - } + handleBeaconLocationData(event, realm, roomId, isLocalEcho) } } } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { @@ -247,12 +241,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } in EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { - handleResponse(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) + } } } in EventType.POLL_END -> { - event.content.toModel(catchError = true)?.let { - handleEndPoll(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + getPowerLevelsHelper(event.roomId)?.let { + pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) + } } } in EventType.STATE_ROOM_BEACON_INFO -> { @@ -260,6 +258,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor( liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho) } } + in EventType.BEACON_LOCATION_DATA -> { + handleBeaconLocationData(event, realm, roomId, isLocalEcho) + } else -> Timber.v("UnHandled event ${event.eventId}") } } catch (t: Throwable) { @@ -317,22 +318,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } - ContentMapper - .map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent) - ?.toModel() - ?.let { existingPollSummaryContent -> - eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map( - PollSummaryContent( - myVote = existingPollSummaryContent.myVote, - votes = emptyList(), - votesSummary = emptyMap(), - totalVotes = 0, - winnerVoteCount = 0, - ) - .toContent() - ) - } - val txId = event.unsignedData?.transactionId // is it a remote echo? if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) { @@ -362,6 +347,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } + if (event.getClearType() in EventType.POLL_START) { + pollAggregationProcessor.handlePollStartEvent(realm, event) + } + if (!isLocalEcho) { val replaceEvent = TimelineEventEntity .where(realm, roomId, eventId) @@ -375,6 +364,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( * Check if the edition is on the latest thread event, and update it accordingly. * @param editedEvent The event that will be changed * @param replaceEvent The new event + * @param editions list of edition of event */ private fun handleThreadSummaryEdition(editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, @@ -391,173 +381,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - private fun handleResponse(realm: Realm, - event: Event, - content: MessagePollResponseContent, - roomId: String, - isLocalEcho: Boolean, - relatedEventId: String? = null) { - val eventId = event.eventId ?: return - val senderId = event.senderId ?: return - val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return - val eventTimestamp = event.originServerTs ?: return - - val targetPollContent = getPollContent(roomId, targetEventId) ?: return - - // ok, this is a poll response - var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst() - if (existing == null) { - Timber.v("## POLL creating new relation summary for $targetEventId") - existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) - } - - // we have it - val existingPollSummary = existing.pollResponseSummary - ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existing.pollResponseSummary = it - } - - val closedTime = existingPollSummary.closedTime - if (closedTime != null && eventTimestamp > closedTime) { - Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") - return - } - - val currentModel = ContentMapper.map(existingPollSummary.aggregatedContent).toModel() - - if (existingPollSummary.sourceEvents.contains(eventId)) { - // ignore this event, we already know it (??) - Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") - return - } - val txId = event.unsignedData?.transactionId - // is it a remote echo? - if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { - // ok it has already been managed - Timber.v("## POLL Receiving remote echo of response eventId:$eventId") - existingPollSummary.sourceLocalEchoEvents.remove(txId) - existingPollSummary.sourceEvents.add(event.eventId) - return - } - - val option = content.getBestResponse()?.answers?.first() ?: return Unit.also { - Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") - } - - // Check if this option is in available options - if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) { - Timber.v("## POLL $targetEventId doesn't contain option $option") - return - } - - val votes = currentModel?.votes.orEmpty().toMutableList() - - var myVote: String? = null - val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } - if (existingVoteIndex != -1) { - // Is the vote newer? - val existingVote = votes[existingVoteIndex] - if (existingVote.voteTimestamp < eventTimestamp) { - // Take the new one - votes[existingVoteIndex] = VoteInfo(senderId, option, eventTimestamp) - if (userId == senderId) { - myVote = option - } - Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") - } else { - Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") - } - } else { - votes.add(VoteInfo(senderId, option, eventTimestamp)) - if (userId == senderId) { - myVote = option - } - Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") - } - - // Precompute the percentage of votes for all options - val totalVotes = votes.size - val newVotesSummary = votes - .groupBy({ it.option }, { it.userId }) - .mapValues { - VoteSummary( - total = it.value.size, - percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes - ) - } - val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total } - - if (isLocalEcho) { - existingPollSummary.sourceLocalEchoEvents.add(eventId) - } else { - existingPollSummary.sourceEvents.add(eventId) - } - - val newSumModel = PollSummaryContent( - myVote = myVote, - votes = votes, - votesSummary = newVotesSummary, - totalVotes = totalVotes, - winnerVoteCount = newWinnerVoteCount - ) - - existingPollSummary.aggregatedContent = ContentMapper.map(newSumModel.toContent()) - } - - private fun handleEndPoll(realm: Realm, - event: Event, - content: MessageEndPollContent, - roomId: String, - isLocalEcho: Boolean) { - val pollEventId = content.relatesTo?.eventId ?: return - val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId - val isPollOwner = pollOwnerId == event.senderId - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? { + return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } - - if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { - Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") - return - } - - var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() - if (existingPoll == null) { - Timber.v("## POLL creating new relation summary for $pollEventId") - existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) - } - - // we have it - val existingPollSummary = existingPoll.pollResponseSummary - ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existingPoll.pollResponseSummary = it - } - - val txId = event.unsignedData?.transactionId - existingPollSummary.closedTime = event.originServerTs - - // is it a remote echo? - if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { - // ok it has already been managed - Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") - existingPollSummary.sourceLocalEchoEvents.remove(txId) - existingPollSummary.sourceEvents.add(event.eventId) - } - } - - private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { - val session = sessionManager.getSessionComponent(sessionId)?.session() - return session?.roomService()?.getRoom(roomId)?.getTimelineEvent(eventId) ?: return null.also { - Timber.v("## POLL target poll event $eventId not found in room $roomId") - } - } - - private fun getPollContent(roomId: String, eventId: String): MessagePollContent? { - val pollEvent = getPollEvent(roomId, eventId) ?: return null - - return pollEvent.getLastMessageContent() as? MessagePollContent ?: return null.also { - Timber.v("## POLL target poll event $eventId content is malformed") - } } private fun handleInitialAggregatedRelations(realm: Realm, @@ -756,4 +583,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor( verifSummary.sourceEvents.add(event.eventId) } } + + private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) { + event.getClearContent().toModel(catchError = true)?.let { + liveLocationAggregationProcessor.handleBeaconLocationData( + realm, + event, + it, + roomId, + event.getRelationContent()?.eventId, + isLocalEcho + ) + } + } } 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 72f56ddf68..ba7f4cf5ad 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 @@ -78,9 +78,9 @@ internal interface RoomAPI { * Get a list of messages starting from a reference. * * @param roomId the room id - * @param from the token identifying where to start. Required. - * @param dir The direction to return messages from. Required. - * @param limit the maximum number of messages to retrieve. Optional. + * @param from the token identifying where to start. Required. + * @param dir The direction to return messages from. Required. + * @param limit the maximum number of messages to retrieve. Optional. * @param filter A JSON RoomEventFilter to filter returned events with. Optional. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages") @@ -94,9 +94,9 @@ internal interface RoomAPI { /** * Get all members of a room. * - * @param roomId the room id where to get the members - * @param syncToken the sync token (optional) - * @param membership to include only one type of membership (optional) + * @param roomId the room id where to get the members + * @param syncToken the sync token (optional) + * @param membership to include only one type of membership (optional) * @param notMembership to exclude one type of membership (optional) */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members") @@ -109,10 +109,10 @@ internal interface RoomAPI { /** * Send an event to a room. * - * @param txId the transaction Id - * @param roomId the room id + * @param txId the transaction Id + * @param roomId the room id * @param eventType the event type - * @param content the event content + * @param content the event content */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") suspend fun send(@Path("txId") txId: String, @@ -124,10 +124,10 @@ internal interface RoomAPI { /** * Get the context surrounding an event. * - * @param roomId the room id + * @param roomId the room id * @param eventId the event Id - * @param limit the maximum number of messages to retrieve - * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + * @param limit the maximum number of messages to retrieve + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}") suspend fun getContextOfEvent(@Path("roomId") roomId: String, @@ -138,7 +138,7 @@ internal interface RoomAPI { /** * Retrieve an event from its room id / events id. * - * @param roomId the room id + * @param roomId the room id * @param eventId the event Id */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") @@ -148,7 +148,7 @@ internal interface RoomAPI { /** * Send read markers. * - * @param roomId the room id + * @param roomId the room id * @param markers the read markers */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") @@ -169,7 +169,7 @@ internal interface RoomAPI { * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-rooms-roomid-invite * * @param roomId the room id - * @param body a object that just contains a user id + * @param body a object that just contains a user id */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") suspend fun invite(@Path("roomId") roomId: String, @@ -179,6 +179,7 @@ internal interface RoomAPI { * Invite a user to a room, using a ThreePid * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101 * @param roomId Required. The room identifier (not alias) to which to invite the user. + * @param body the Json body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") suspend fun invite3pid(@Path("roomId") roomId: String, @@ -187,9 +188,9 @@ internal interface RoomAPI { /** * Send a generic state event. * - * @param roomId the room id. + * @param roomId the room id. * @param stateEventType the state event type - * @param params the request parameters + * @param params the request parameters */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}") suspend fun sendStateEvent(@Path("roomId") roomId: String, @@ -200,10 +201,10 @@ internal interface RoomAPI { /** * Send a generic state event. * - * @param roomId the room id. + * @param roomId the room id. * @param stateEventType the state event type - * @param stateKey the state keys - * @param params the request parameters + * @param stateKey the state keys + * @param params the request parameters */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}") suspend fun sendStateEvent(@Path("roomId") roomId: String, @@ -221,8 +222,13 @@ internal interface RoomAPI { /** * Paginate relations for event based in normal topological order. + * @param roomId the room Id + * @param eventId the event Id * @param relationType filter for this relation type * @param eventType filter for this event type + * @param from from token + * @param to to token + * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") suspend fun getRelations(@Path("roomId") roomId: String, @@ -236,7 +242,13 @@ internal interface RoomAPI { /** * Paginate relations for thread events based in normal topological order. + * + * @param roomId the room Id + * @param eventId the event Id * @param relationType filter for this relation type + * @param from from token + * @param to to token + * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") suspend fun getThreadsRelations(@Path("roomId") roomId: String, @@ -262,7 +274,7 @@ internal interface RoomAPI { /** * Leave the given room. * - * @param roomId the room id + * @param roomId the room id * @param params the request body */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") @@ -272,7 +284,7 @@ internal interface RoomAPI { /** * Ban a user from the given room. * - * @param roomId the room id + * @param roomId the room id * @param userIdAndReason the banned user object (userId and reason for ban) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban") @@ -282,7 +294,7 @@ internal interface RoomAPI { /** * unban a user from the given room. * - * @param roomId the room id + * @param roomId the room id * @param userIdAndReason the unbanned user object (userId and reason for unban) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban") @@ -292,7 +304,7 @@ internal interface RoomAPI { /** * Kick a user from the given room. * - * @param roomId the room id + * @param roomId the room id * @param userIdAndReason the kicked user object (userId and reason for kicking) */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick") @@ -304,10 +316,10 @@ internal interface RoomAPI { * This cannot be undone. * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. * - * @param txId the transaction Id - * @param roomId the room id - * @param eventId the event to delete - * @param reason json containing reason key {"reason": "Indecent material"} + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete + * @param reason json containing reason key {"reason": "Indecent material"} */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") suspend fun redactEvent( @@ -320,9 +332,9 @@ internal interface RoomAPI { /** * Reports an event as inappropriate to the server, which may then notify the appropriate people. * - * @param roomId the room id + * @param roomId the room id * @param eventId the event to report content - * @param body body containing score and reason + * @param body body containing score and reason */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") suspend fun reportContent(@Path("roomId") roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index 29a303475b..c3d55b267a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -35,7 +35,7 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId /** * Compute the room avatar url. - * @param realm: the current instance of realm + * @param realm the current instance of realm * @param roomId the roomId of the room to resolve avatar * @return the room avatar url, can be a fallback to a room member avatar or null */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 01c4fd1501..ffe7679575 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService import org.matrix.android.sdk.internal.session.room.crypto.DefaultRoomCryptoService import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService +import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService import org.matrix.android.sdk.internal.session.room.read.DefaultReadService @@ -69,6 +70,7 @@ internal class DefaultRoomFactory @Inject constructor( private val roomVersionServiceFactory: DefaultRoomVersionService.Factory, private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val viaParameterFinder: ViaParameterFinder, + private val locationSharingServiceFactory: DefaultLocationSharingService.Factory, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : RoomFactory { @@ -96,6 +98,7 @@ internal class DefaultRoomFactory @Inject constructor( roomAccountDataService = roomAccountDataServiceFactory.create(roomId), roomVersionService = roomVersionServiceFactory.create(roomId), viaParameterFinder = viaParameterFinder, + locationSharingService = locationSharingServiceFactory.create(roomId), coroutineDispatchers = coroutineDispatchers ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt new file mode 100644 index 0000000000..2b83c8028b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DeactivateLiveLocationShareWorker.kt @@ -0,0 +1,97 @@ +/* + * 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.session.room.aggregation.livelocation + +import android.content.Context +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.util.md5 +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import timber.log.Timber +import javax.inject.Inject + +/** + * Worker dedicated to update live location summary data so that it is considered as deactivated. + * For the context: it is needed since a live location share should be deactivated after a certain timeout. + */ +internal class DeactivateLiveLocationShareWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : + SessionSafeCoroutineWorker( + context, + params, + sessionManager, + Params::class.java + ) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + override val lastFailureMessage: String? = null, + val eventId: String, + val roomId: String + ) : SessionWorkerParams + + @SessionDatabase + @Inject lateinit var realmConfiguration: RealmConfiguration + + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + return runCatching { + deactivateLiveLocationShare(params) + }.fold( + onSuccess = { + Result.success() + }, + onFailure = { + Timber.e("failed to deactivate live, eventId: ${params.eventId}, roomId: ${params.roomId}") + Result.failure() + } + ) + } + + private suspend fun deactivateLiveLocationShare(params: Params) { + awaitTransaction(realmConfiguration) { realm -> + val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.get( + realm = realm, + roomId = params.roomId, + eventId = params.eventId + ) + aggregatedSummary?.isActive = false + } + } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + + companion object { + fun getWorkName(eventId: String, roomId: String): String { + val hash = "$eventId$roomId".md5() + return "DeactivateLiveLocationWork-$hash" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt deleted file mode 100644 index 997e31a109..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.aggregation.livelocation - -import io.realm.Realm -import org.matrix.android.sdk.api.extensions.orTrue -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity -import org.matrix.android.sdk.internal.database.query.getOrCreate -import timber.log.Timber -import javax.inject.Inject - -internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : LiveLocationAggregationProcessor { - - override fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) { - if (event.senderId.isNullOrEmpty() || isLocalEcho) { - return - } - - val targetEventId = if (content.isLive.orTrue()) { - event.eventId - } else { - // when live is set to false, we use the id of the event that should have been replaced - event.unsignedData?.replacesState - } - - if (targetEventId.isNullOrEmpty()) { - Timber.w("no target event id found for the beacon content") - return - } - - val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( - realm = realm, - roomId = roomId, - eventId = targetEventId - ) - - Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}") - - aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } - aggregatedSummary.isActive = content.isLive - } - - override fun handleBeaconLocationData(realm: Realm, event: Event, content: MessageBeaconLocationDataContent, roomId: String, isLocalEcho: Boolean) { - if (event.senderId.isNullOrEmpty() || isLocalEcho) { - return - } - - val targetEventId = content.relatesTo?.eventId - - if (targetEventId.isNullOrEmpty()) { - Timber.w("no target event id found for the live location content") - return - } - - val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( - realm = realm, - roomId = roomId, - eventId = targetEventId - ) - val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 - val currentLocationTimestamp = ContentMapper - .map(aggregatedSummary.lastLocationContent) - .toModel() - ?.getBestTimestampMillis() - ?: 0 - - if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) { - Timber.d("updating last location of the summary of id=$targetEventId") - aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent()) - } - } - - private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index c0be96f83d..8f4682a9d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -16,25 +16,142 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation +import androidx.work.ExistingWorkPolicy import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.findActiveLiveInRoomForUser +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject -internal interface LiveLocationAggregationProcessor { - fun handleBeaconInfo( - realm: Realm, - event: Event, - content: MessageBeaconInfoContent, - roomId: String, - isLocalEcho: Boolean, - ) +// TODO add unit tests +internal class LiveLocationAggregationProcessor @Inject constructor( + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, + private val clock: Clock, +) { + + fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) { + if (event.senderId.isNullOrEmpty() || isLocalEcho) { + return + } + + val isLive = content.isLive.orTrue() + val targetEventId = if (isLive) { + event.eventId + } else { + // when live is set to false, we use the id of the event that should have been replaced + event.unsignedData?.replacesState + } + + if (targetEventId.isNullOrEmpty()) { + Timber.w("no target event id found for the beacon content") + return + } + + val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( + realm = realm, + roomId = roomId, + eventId = targetEventId + ) + + Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}") + + val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } + aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis + aggregatedSummary.isActive = isLive + aggregatedSummary.userId = event.senderId + + deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId) + + if (isLive) { + scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis) + } else { + cancelDeactivationAfterTimeout(targetEventId, roomId) + } + } + + private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) { + endOfLiveTimestampMillis ?: return + + val workParams = DeactivateLiveLocationShareWorker.Params(sessionId = sessionId, eventId = eventId, roomId = roomId) + val workData = WorkerParamsFactory.toData(workParams) + val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId) + val workDelayMillis = (endOfLiveTimestampMillis - clock.epochMillis()).coerceAtLeast(0) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setInitialDelay(workDelayMillis, TimeUnit.MILLISECONDS) + .setInputData(workData) + .build() + + workManagerProvider.workManager.enqueueUniqueWork( + workName, + ExistingWorkPolicy.REPLACE, + workRequest + ) + } + + private fun cancelDeactivationAfterTimeout(eventId: String, roomId: String) { + val workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = eventId, roomId = roomId) + workManagerProvider.workManager.cancelUniqueWork(workName) + } fun handleBeaconLocationData( realm: Realm, event: Event, content: MessageBeaconLocationDataContent, roomId: String, - isLocalEcho: Boolean, - ) + relatedEventId: String?, + isLocalEcho: Boolean + ) { + if (event.senderId.isNullOrEmpty() || isLocalEcho) { + return + } + + if (relatedEventId.isNullOrEmpty()) { + Timber.w("no related event id found for the live location content") + return + } + + val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate( + realm = realm, + roomId = roomId, + eventId = relatedEventId + ) + val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 + val currentLocationTimestamp = ContentMapper + .map(aggregatedSummary.lastLocationContent) + .toModel() + ?.getBestTimestampMillis() + ?: 0 + + if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) { + Timber.d("updating last location of the summary of id=$relatedEventId") + aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent()) + } + } + + private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { + LiveLocationShareAggregatedSummaryEntity + .findActiveLiveInRoomForUser( + realm = realm, + roomId = roomId, + userId = userId, + ignoredEventId = currentEventId + ) + .forEach { it.isActive = false } + } + + private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt new file mode 100644 index 0000000000..d4b414aaea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -0,0 +1,203 @@ +/* + * 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.aggregation.poll + +import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.api.session.room.model.PollSummaryContent +import org.matrix.android.sdk.api.session.room.model.VoteInfo +import org.matrix.android.sdk.api.session.room.model.VoteSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.create +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import javax.inject.Inject + +class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor { + + override fun handlePollStartEvent(realm: Realm, event: Event): Boolean { + val content = event.getClearContent()?.toModel() + if (content?.relatesTo?.type != RelationType.REPLACE) { + return false + } + + val roomId = event.roomId ?: return false + val targetEventId = content.relatesTo.eventId ?: return false + + EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId).let { eventAnnotationsSummaryEntity -> + ContentMapper + .map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent) + ?.toModel() + ?.let { existingPollSummaryContent -> + eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map( + PollSummaryContent( + myVote = existingPollSummaryContent.myVote, + votes = emptyList(), + votesSummary = emptyMap(), + totalVotes = 0, + winnerVoteCount = 0, + ) + .toContent() + ) + } + } + return true + } + + override fun handlePollResponseEvent(session: Session, realm: Realm, event: Event): Boolean { + val content = event.getClearContent()?.toModel() ?: return false + val roomId = event.roomId ?: return false + val senderId = event.senderId ?: return false + val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false + val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false + + val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) + val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity) + + val closedTime = aggregatedPollSummaryEntity.closedTime + val responseTime = event.originServerTs ?: return false + if (closedTime != null && responseTime > closedTime) { + return false + } + + if (aggregatedPollSummaryEntity.sourceEvents.contains(event.eventId)) { + return false + } + + val txId = event.unsignedData?.transactionId + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId) + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + return false + } + + val vote = content.getBestResponse()?.answers?.first() ?: return false + if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(vote).orFalse()) { + return false + } + + val pollSummaryModel = ContentMapper.map(aggregatedPollSummaryEntity.aggregatedContent).toModel() + val existingVotes = pollSummaryModel?.votes.orEmpty().toMutableList() + val existingVoteIndex = existingVotes.indexOfFirst { it.userId == senderId } + + if (existingVoteIndex != -1) { + val existingVote = existingVotes[existingVoteIndex] + if (existingVote.voteTimestamp > responseTime) { + return false + } + existingVotes[existingVoteIndex] = VoteInfo(senderId, vote, responseTime) + } else { + existingVotes.add(VoteInfo(senderId, vote, responseTime)) + } + + // Precompute the percentage of votes for all options + val totalVotes = existingVotes.size + val newVotesSummary = existingVotes + .groupBy({ it.option }, { it.userId }) + .mapValues { + VoteSummary( + total = it.value.size, + percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes + ) + } + val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total } + + if (isLocalEcho) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.add(event.eventId) + } else { + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + } + + val myVote = existingVotes.find { it.userId == session.myUserId }?.option + + val newSumModel = PollSummaryContent( + myVote = myVote, + votes = existingVotes, + votesSummary = newVotesSummary, + totalVotes = totalVotes, + winnerVoteCount = newWinnerVoteCount + ) + aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent()) + + return true + } + + override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean { + val content = event.getClearContent()?.toModel() ?: return false + val roomId = event.roomId ?: return false + val pollEventId = content.relatesTo?.eventId ?: return false + val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId + val isPollOwner = pollOwnerId == event.senderId + + if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) { + return false + } + + val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, pollEventId) + val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity) + + val txId = event.unsignedData?.transactionId + aggregatedPollSummaryEntity.closedTime = event.originServerTs + + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId) + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + } + + return true + } + + private fun getPollEvent(session: Session, roomId: String, eventId: String): TimelineEvent? { + return session.roomService().getRoom(roomId)?.getTimelineEvent(eventId) + } + + private fun getPollContent(session: Session, roomId: String, eventId: String): MessagePollContent? { + val pollEvent = getPollEvent(session, roomId, eventId) + return pollEvent?.getLastMessageContent() as? MessagePollContent + } + + private fun getAnnotationsSummaryEntity(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) + } + + private fun getAggregatedPollSummaryEntity(realm: Realm, + eventAnnotationsSummaryEntity: EventAnnotationsSummaryEntity): PollResponseAggregatedSummaryEntity { + return eventAnnotationsSummaryEntity.pollResponseSummary + ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { + eventAnnotationsSummaryEntity.pollResponseSummary = it + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt new file mode 100644 index 0000000000..848643b435 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.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.aggregation.poll + +import io.realm.Realm +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper + +interface PollAggregationProcessor { + /** + * Poll start events don't need to be processed by the aggregator. + * This function will only handle if the poll is edited and will update the poll summary entity. + * Returns true if the event is aggregated. + */ + fun handlePollStartEvent( + realm: Realm, + event: Event + ): Boolean + + /** + * Aggregates poll response event after many conditional checks like if the poll is ended, if the user is changing his/her vote etc. + * Returns true if the event is aggregated. + */ + fun handlePollResponseEvent( + session: Session, + realm: Realm, + event: Event + ): Boolean + + /** + * Updates poll summary entity and mark it is ended after many conditional checks like if the poll is already ended etc. + * Returns true if the event is aggregated. + */ + fun handlePollEndEvent( + session: Session, + powerLevelsHelper: PowerLevelsHelper, + realm: Realm, + event: Event + ): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt index 7c137a8102..fa19b4f9cf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.internal.session.room.alias -import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.internal.di.UserId @@ -65,6 +65,6 @@ internal class RoomAliasAvailabilityChecker @Inject constructor( } companion object { - internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getDomain() + internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getServerName() } } 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 new file mode 100644 index 0000000000..8cf6fcdfbf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -0,0 +1,49 @@ +/* + * 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.session.room.location + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import org.matrix.android.sdk.api.session.room.location.LocationSharingService +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom +import org.matrix.android.sdk.internal.di.SessionDatabase + +// TODO add unit tests +internal class DefaultLocationSharingService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, +) : LocationSharingService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultLocationSharingService + } + + override fun getRunningLiveLocationShareSummaries(): LiveData> { + return monarchy.findAllMappedWithChanges( + { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, + { liveLocationShareAggregatedSummaryMapper.map(it) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 59e0f81ece..9e672dcc5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -52,8 +52,8 @@ internal class RoomDisplayNameResolver @Inject constructor( /** * Compute the room display name. * - * @param realm: the current instance of realm - * @param roomId: the roomId to resolve the name of. + * @param realm the current instance of realm + * @param roomId the roomId to resolve the name of. * @return the room display name */ fun resolve(realm: Realm, roomId: String): RoomName { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt index 5bad334afc..fffca96acf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt @@ -43,7 +43,6 @@ internal class RoomChildRelationInfo( data class SpaceChildInfo( val roomId: String, val order: String?, -// val autoJoin: Boolean, val viaServers: List ) @@ -60,18 +59,13 @@ internal class RoomChildRelationInfo( fun getDirectChildrenDescriptions(): List { return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) .findAll() -// .also { -// Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId") -// } .mapNotNull { ContentMapper.map(it.root?.content).toModel()?.let { scc -> -// Timber.v("## Space child desc state event $scc") // Children where via is not present are ignored. scc.via?.let { via -> SpaceChildInfo( roomId = it.stateKey, order = scc.validOrder(), -// autoJoin = scc.autoJoin ?: false, viaServers = via ) } @@ -83,17 +77,13 @@ internal class RoomChildRelationInfo( fun getParentDescriptions(): List { return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT) .findAll() -// .also { -// Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId") -// } .mapNotNull { - ContentMapper.map(it.root?.content).toModel()?.let { scc -> -// Timber.v("## Space parent desc state event $scc") + ContentMapper.map(it.root?.content).toModel()?.let { spaceParentContent -> // Parent where via is not present are ignored. - scc.via?.let { via -> + spaceParentContent.via?.let { via -> SpaceParentInfo( roomId = it.stateKey, - canonical = scc.canonical ?: false, + canonical = spaceParentContent.canonical ?: false, viaServers = via, stateEventSender = it.root?.sender ?: "" ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt index 948786677d..983701857f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt @@ -21,8 +21,8 @@ import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger /** - * @param queueIdentifier String value to identify a unique Queue - * @param taskIdentifier String value to identify a unique Task. Should be different from queueIdentifier + * @property queueIdentifier String value to identify a unique Queue + * @property taskIdentifier String value to identify a unique Task. Should be different from queueIdentifier */ internal abstract class QueuedTask( val queueIdentifier: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 96e8d3c73f..4b453908e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -26,8 +26,9 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where -import org.matrix.android.sdk.api.query.ActiveSpaceFilter +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.query.isNormalized import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.RoomSortOrder @@ -187,13 +188,14 @@ internal class RoomSummaryDataSource @Inject constructor( fun getUpdatablePagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, - sortOrder: RoomSortOrder): UpdatableLivePageResult { + sortOrder: RoomSortOrder, + getFlattenedParents: Boolean = false): UpdatableLivePageResult { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> roomSummariesQuery(realm, queryParams).process(sortOrder) } val dataSourceFactory = realmDataSourceFactory.map { roomSummaryMapper.map(it) - } + }.map { if (getFlattenedParents) it.getWithParents() else it } val boundaries = MutableLiveData(ResultBoundaries()) @@ -232,6 +234,13 @@ internal class RoomSummaryDataSource @Inject constructor( } } + private fun RoomSummary.getWithParents(): RoomSummary { + val parents = flattenParentIds.mapNotNull { parentId -> + getRoomSummary(parentId) + } + return copy(flattenParents = parents) + } + fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData { val liveRooms = monarchy.findAllManagedWithChanges { roomSummariesQuery(it, queryParams) @@ -258,29 +267,13 @@ internal class RoomSummaryDataSource @Inject constructor( private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { val query = with(queryStringValueProcessor) { RoomSummaryEntity.where(realm) - .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) - .let { - if (queryParams.displayName.isNormalized()) { - it.process(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, queryParams.displayName) - } else { - it.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) - } - } + .process(RoomSummaryEntityFields.ROOM_ID, QueryStringValue.IsNotEmpty) + .process(queryParams.displayName.toDisplayNameField(), queryParams.displayName) .process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) .process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) .equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false) } - queryParams.roomCategoryFilter?.let { - when (it) { - RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) - RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) - RoomCategoryFilter.ALL -> { - // nop - } - } - } queryParams.roomTagQueryFilter?.let { it.isFavorite?.let { fav -> query.equalTo(RoomSummaryEntityFields.IS_FAVOURITE, fav) @@ -303,28 +296,24 @@ internal class RoomSummaryDataSource @Inject constructor( RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) - RoomCategoryFilter.ALL -> Unit // nop null -> Unit } // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}") - when (queryParams.activeSpaceFilter) { - is ActiveSpaceFilter.ActiveSpace -> { + when (queryParams.spaceFilter) { + SpaceFilter.OrphanRooms -> { + // orphan rooms + query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS) + } + is SpaceFilter.ActiveSpace -> { // It's annoying but for now realm java does not support querying in primitive list :/ // https://github.com/realm/realm-java/issues/5361 - if (queryParams.activeSpaceFilter.currentSpaceId == null) { - // orphan rooms - query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS) - } else { - query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.currentSpaceId) - } + query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId) } - is ActiveSpaceFilter.ExcludeSpace -> { - query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.spaceId) - } - else -> { - // nop + is SpaceFilter.ExcludeSpace -> { + query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId) } + null -> Unit // nop } queryParams.activeGroupId?.let { activeGroupId -> @@ -333,6 +322,14 @@ internal class RoomSummaryDataSource @Inject constructor( return query } + private fun QueryStringValue.toDisplayNameField(): String { + return if (isNormalized()) { + RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME + } else { + RoomSummaryEntityFields.DISPLAY_NAME + } + } + fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List): List { val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList() val result = ArrayList() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt index d8daa55e15..33c3c3929f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt @@ -24,11 +24,12 @@ import retrofit2.http.Query internal interface SpaceApi { /** + * @param spaceId the space Id * @param suggestedOnly Optional. If true, return only child events and rooms where the m.space.child event has suggested: true. - * @param limit: Optional: a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer. - * @param maxDepth: Optional: The maximum depth in the tree (from the root room) to return. + * @param limit Optional: a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer. + * @param maxDepth Optional: The maximum depth in the tree (from the root room) to return. * The deepest depth returned will not include children events. Defaults to no-limit. Must be a non-negative integer. - * @param from: Optional. Pagination token given to retrieve the next set of rooms. + * @param from Optional. Pagination token given to retrieve the next set of rooms. * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same. */ @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/hierarchy") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index dd95762166..e5a5a0bbad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -62,7 +62,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: /** * Decrypt an encrypted event. * - * @param event the event to decrypt + * @param event the event to decrypt * @param timelineId the timeline identifier * @return true if the event has been decrypted */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index c5d14afac0..53fc9dc6b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -520,9 +520,10 @@ internal class RoomSyncHandler @Inject constructor( private fun decryptIfNeeded(event: Event, roomId: String) { try { + val timelineId = generateTimelineId(roomId) // Event from sync does not have roomId, so add it to the event first // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching - val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") } + val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) } event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, @@ -537,6 +538,10 @@ internal class RoomSyncHandler @Inject constructor( } } + private fun generateTimelineId(roomId: String): String { + return "RoomSyncHandler$roomId" + } + data class EphemeralResult( val typingUserIds: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 03e076c217..9beb8333a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -206,6 +206,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( /** * Handle for not thread events that we have marked them as root. * Find relations and inject them accordingly + * @param realm the realm instance + * @param roomId the current room Id * @param eventEntity the current eventEntity received * @param event the current event received * @return The content to inject in the roomSyncHandler live events @@ -229,9 +231,12 @@ internal class ThreadsAwarenessHandler @Inject constructor( * This function is responsible to check if there is any event that relates to our current event. * This is useful when we receive an event that relates to a missing parent, so when later we receive the parent * we can update the child as well. + * @param realm the realm instance + * @param roomId the current room Id * @param event the current event that we examine * @param eventBody the current body of the event * @param isFromCache determines whether or not we already know this is root thread event + * @param threadRelation the information about thread * @return The content to inject in the roomSyncHandler live events */ private fun handleEventsThatRelatesTo( @@ -291,9 +296,12 @@ internal class ThreadsAwarenessHandler @Inject constructor( } /** - * Injecting $eventToInject decrypted content as a reply to $event. - * @param eventToInject the event that will inject + * Injecting [eventToInject] decrypted content as a reply to event. + * @param roomId the room id * @param eventBody the actual event body + * @param eventToInject the event that will inject + * @param eventToInjectBody the event body to inject + * @param threadRelation the information about thread * @return The final content with the injected event */ private fun injectEvent(roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt index bbeff18c01..178f349ec8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -27,7 +27,7 @@ internal interface AccountDataAPI { * Set some account_data for the client. * * @param userId the user id - * @param type the type + * @param type the type * @param params the put params */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt index 1da6827916..857105f6ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -95,7 +95,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh /** * Send a boolean response. * - * @param response the response + * @param response the response * @param eventData the modular data */ override fun sendBoolResponse(response: Boolean, eventData: JsonDict) { @@ -106,7 +106,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh /** * Send an integer response. * - * @param response the response + * @param response the response * @param eventData the modular data */ override fun sendIntegerResponse(response: Int, eventData: JsonDict) { @@ -116,7 +116,9 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh /** * Send an object response. * - * @param response the response + * @param T the Json type + * @param type the type + * @param response the response * @param eventData the modular data */ override fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) { @@ -145,7 +147,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh /** * Send an error. * - * @param message the error message + * @param message the error message * @param eventData the modular data */ override fun sendError(message: String, eventData: JsonDict) { @@ -162,7 +164,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh /** * Send the response to the javascript. * - * @param jsString the response data + * @param jsString the response data * @param eventData the modular data */ private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt index b871a317c8..97b40e545e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -27,6 +27,7 @@ internal interface WidgetsAPI { * Register to the server. * * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) + * @param version the widget API version */ @POST("register") suspend fun register(@Body body: OpenIdToken, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt index 80081e3186..dd4c5e7623 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.withPermit */ internal interface CoroutineSequencer { /** + * @param T generic type * @param block the suspendable block to execute * @return the result of the block */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt index 94aa238789..c50b7fe675 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt @@ -53,7 +53,7 @@ internal object JsonCanonicalizer { /** * Canonicalize a JSON element. * - * @param src the src + * @param any the src * @return the canonicalize element */ private fun canonicalizeRecursive(any: Any): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index d9fd312a6f..c6a417f6eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -54,7 +54,7 @@ internal fun convertFromUTF8(s: String): String { /** * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. * - * @param subString the string to search for + * @param subString the string to search for * @return whether a match was found */ internal fun String.caseInsensitiveFind(subString: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index 0d4a5ac28f..31549155d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -75,7 +75,8 @@ internal class DefaultGetWellknownTask @Inject constructor( * - validate homeserver url and identity server url if provide in .well-known result * - return action and .well-known data * - * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + * @param domain homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + * @param client Http client to perform the request */ private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult { val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt index 95b3662c67..a8ef3e0748 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker @@ -64,9 +65,11 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage SyncWorker(appContext, workerParameters, sessionManager) UpdateTrustWorker::class.java.name -> UpdateTrustWorker(appContext, workerParameters, sessionManager) - UploadContentWorker::class.java.name -> + UploadContentWorker::class.java.name -> UploadContentWorker(appContext, workerParameters, sessionManager) - else -> { + DeactivateLiveLocationShareWorker::class.java.name -> + DeactivateLiveLocationShareWorker(appContext, workerParameters, sessionManager) + else -> { Timber.w("No worker defined on MatrixWorkerFactory for $workerClassName will delegate to default.") // Return null to delegate to the default WorkerFactory. null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt new file mode 100644 index 0000000000..06c6cc9ef9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us000InitMatrix.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Init a Matrix object. + * + * ### Required APIs: + * - [org.matrix.android.sdk.api.Matrix] constructor + */ +class Us000InitMatrix private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt new file mode 100644 index 0000000000..f508a75db7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us100SignIn.kt @@ -0,0 +1,31 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Sign in to an existing account. + * + * #### Required APIs: + * - [org.matrix.android.sdk.api.Matrix.authenticationService] + * - [org.matrix.android.sdk.api.auth.AuthenticationService.getLoginFlow] + * - [org.matrix.android.sdk.api.auth.AuthenticationService.getLoginWizard] + * - [org.matrix.android.sdk.api.auth.login.LoginWizard.login] + */ +class Us100SignIn private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt new file mode 100644 index 0000000000..ac56cc35dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us150VerifySession.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Verify a Session after a Sign in. + * + * #### Required APIs: + * - TODO + */ +class Us150VerifySession private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt new file mode 100644 index 0000000000..0c74c0dfef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us190SignOut.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Sign out. + * + * #### Required APIs: + * - TODO + */ +class Us190SignOut private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt new file mode 100644 index 0000000000..1f0b2e4103 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us200RoomList.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Get the Room list. + * + * #### Required APIs: + * - TODO + */ +class Us200RoomList private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt new file mode 100644 index 0000000000..5db06f95f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us300RoomTimeline.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Display a Room timeline, and navigate backward and forward. + * + * #### Required APIs: + * - TODO + */ +class Us300RoomTimeline private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt new file mode 100644 index 0000000000..5e0f6d3b3d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us350RoomTimelineFromPermalink.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Display a Room timeline at a specific point. + * + * #### Required APIs: + * - TODO + */ +class Us350RoomTimelineFromPermalink private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt new file mode 100644 index 0000000000..3f72cd0b9e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us400RoomSendContent.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Send content to a room, including monitoring the sending state. + * + * #### Required APIs: + * - TODO + */ +class Us400RoomSendContent private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt new file mode 100644 index 0000000000..c5c2107fa6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us500Notification.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Get notified when new Events are received. + * + * #### Required APIs: + * - TODO + */ +class Us500Notification private constructor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt new file mode 100644 index 0000000000..6b8fb87df7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/userstories/Us600SyncWithTheServer.kt @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@file:Suppress("unused") + +package org.matrix.android.sdk.userstories + +/** + * ### Title + * Manage the sync with the server. + * + * #### Required APIs: + * - TODO + */ +class Us600SyncWithTheServer private constructor() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt new file mode 100644 index 0000000000..47d5f46525 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.Moshi +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.message.LocationInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity + +private const val ANY_USER_ID = "a-user-id" +private const val ANY_ACTIVE_STATE = true +private const val ANY_TIMEOUT = 123L +private val A_LOCATION_INFO = LocationInfo("a-geo-uri") + +class LiveLocationShareAggregatedSummaryMapperTest { + + private val mapper = LiveLocationShareAggregatedSummaryMapper() + + @Test + fun `given an entity then result should be mapped correctly`() { + val entity = anEntity(content = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO)) + + val summary = mapper.map(entity) + + summary shouldBeEqualTo LiveLocationShareAggregatedSummary( + userId = ANY_USER_ID, + isActive = ANY_ACTIVE_STATE, + endOfLiveTimestampMillis = ANY_TIMEOUT, + lastLocationDataContent = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO) + ) + } + + private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity( + userId = ANY_USER_ID, + isActive = ANY_ACTIVE_STATE, + endOfLiveTimestampMillis = ANY_TIMEOUT, + lastLocationContent = Moshi.Builder().build().adapter(MessageBeaconLocationDataContent::class.java).toJson(content) + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt new file mode 100644 index 0000000000..837bbeea26 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt @@ -0,0 +1,162 @@ +/* + * 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.aggregation.poll + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import io.realm.RealmModel +import io.realm.RealmQuery +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.Session +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.getTimelineEvent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_RESPONSE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_START_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 +import org.matrix.android.sdk.test.fakes.FakeRealm + +class PollAggregationProcessorTest { + + private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() + private val realm = FakeRealm() + private val session = mockk() + + @Before + fun setup() { + mockEventAnnotationsSummaryEntity() + mockRoom(A_ROOM_ID, AN_EVENT_ID) + every { session.myUserId } returns A_USER_ID_1 + } + + @Test + fun `given a poll start event, when processing, then is ignored and returns false`() { + pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_START_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll start event with a reference, when processing, then is ignored and returns false`() { + pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_REFERENCE_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll start event with a replace relation but without a target event id, when processing, then is ignored and returns false`() { + pollAggregationProcessor.handlePollStartEvent(realm.instance, A_BROKEN_POLL_REPLACE_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll start event with a replace, when processing, then is processed and returns true`() { + pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_REPLACE_EVENT).shouldBeTrue() + } + + @Test + fun `given a poll response event with a broken reference, when processing, then is ignored and returns false`() { + pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE).shouldBeFalse() + } + + @Test + fun `given a poll response event with a reference, when processing, then is processed and returns true`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue() + } + + @Test + fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply { + closedTime = (A_POLL_RESPONSE_EVENT.originServerTs ?: 0) - 1 + } + pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll response event which is already processed, when processing, then is ignored and returns false`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply { + sourceEvents = RealmList(A_POLL_RESPONSE_EVENT.eventId) + } + pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll response event which is not one of the options, when processing, then is ignored and returns false`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, AN_INVALID_POLL_RESPONSE_EVENT).shouldBeFalse() + } + + @Test + fun `given a poll end event, when processing, then is processed and return true`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() + } + + @Test + fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() + } + + @Test + fun `given a poll end event without enough redaction power level, when is processed, then is ignored and return false`() { + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", false) + val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id") + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() + } + + private inline fun RealmQuery.givenEqualTo(fieldName: String, value: String, result: RealmQuery) { + every { equalTo(fieldName, value) } returns result + } + + private fun mockEventAnnotationsSummaryEntity() { + val queryResult = realm.givenWhereReturns(result = EventAnnotationsSummaryEntity()) + queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!, queryResult) + queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!, queryResult) + } + + private fun mockRoom( + roomId: String, + eventId: String + ) { + val room = mockk() + every { session.getRoom(roomId) } returns room + every { room.getTimelineEvent(eventId) } returns A_TIMELINE_EVENT + } + + private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper { + val powerLevelsHelper = mockk() + every { powerLevelsHelper.isUserAbleToRedact(userId) } returns isAbleToRedact + return powerLevelsHelper + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt new file mode 100644 index 0000000000..129d49633e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt @@ -0,0 +1,171 @@ +/* + * 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.aggregation.poll + +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.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollResponse +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +object PollEventsTestData { + internal const val A_USER_ID_1 = "@user_1:matrix.org" + internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org" + internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU" + + internal val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = "Macchiato" + ), + PollAnswer( + id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", + unstableAnswer = "Iced Coffee" + ) + ) + ) + ) + + internal val A_POLL_RESPONSE_CONTENT = MessagePollResponseContent( + unstableResponse = PollResponse( + answers = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76") + ), + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = AN_EVENT_ID + ) + ) + + internal val A_POLL_END_CONTENT = MessageEndPollContent( + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = AN_EVENT_ID + ) + ) + + internal val AN_INVALID_POLL_RESPONSE_CONTENT = MessagePollResponseContent( + unstableResponse = PollResponse( + answers = listOf("fake-option-id") + ), + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = AN_EVENT_ID + ) + ) + + internal val A_POLL_START_EVENT = Event( + type = EventType.POLL_START.first(), + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID, + content = A_POLL_CONTENT.toContent() + ) + + internal val A_POLL_RESPONSE_EVENT = Event( + type = EventType.POLL_RESPONSE.first(), + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID, + content = A_POLL_RESPONSE_CONTENT.toContent() + ) + + internal val A_POLL_END_EVENT = Event( + type = EventType.POLL_END.first(), + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID, + content = A_POLL_END_CONTENT.toContent() + ) + + internal val A_TIMELINE_EVENT = TimelineEvent( + root = A_POLL_START_EVENT, + localId = 1234, + eventId = AN_EVENT_ID, + displayIndex = 0, + senderInfo = SenderInfo(A_USER_ID_1, "A_USER_ID_1", true, null) + ) + + internal val A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE = A_POLL_RESPONSE_EVENT.copy( + content = A_POLL_RESPONSE_CONTENT + .copy( + relatesTo = RelationDefaultContent( + type = RelationType.REPLACE, + eventId = null + ) + ) + .toContent() + ) + + internal val A_POLL_REPLACE_EVENT = A_POLL_START_EVENT.copy( + content = A_POLL_CONTENT + .copy( + relatesTo = RelationDefaultContent( + type = RelationType.REPLACE, + eventId = AN_EVENT_ID + ) + ) + .toContent() + ) + + internal val A_BROKEN_POLL_REPLACE_EVENT = A_POLL_START_EVENT.copy( + content = A_POLL_CONTENT + .copy( + relatesTo = RelationDefaultContent( + type = RelationType.REPLACE, + eventId = null + ) + ) + .toContent() + ) + + internal val A_POLL_REFERENCE_EVENT = A_POLL_START_EVENT.copy( + content = A_POLL_CONTENT + .copy( + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = AN_EVENT_ID + ) + ) + .toContent() + ) + + internal val AN_INVALID_POLL_RESPONSE_EVENT = A_POLL_RESPONSE_EVENT.copy( + content = AN_INVALID_POLL_RESPONSE_CONTENT.toContent() + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt new file mode 100644 index 0000000000..c07f8e1873 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.every +import io.mockk.mockk +import io.realm.Realm +import io.realm.RealmModel +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal class FakeRealm { + + val instance = mockk(relaxed = true) + + inline fun givenWhereReturns(result: T?): RealmQuery { + val queryResult = mockk>() + every { queryResult.findFirst() } returns result + every { instance.where() } returns queryResult + return queryResult + } +} diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh index e40d3635e8..910616176c 100755 --- a/tools/check/check_code_quality.sh +++ b/tools/check/check_code_quality.sh @@ -67,6 +67,9 @@ echo "Search for forbidden patterns in code..." ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt \ ./matrix-sdk-android/src/main/java \ ./matrix-sdk-android-flow/src/main/java \ + ./library/core-utils/src/main/java \ + ./library/jsonviewer/src/main/java \ + ./library/ui-styles/src/main/java \ ./vector/src/main/java \ ./vector/src/debug/java \ ./vector/src/release/java \ @@ -100,6 +103,7 @@ echo echo "Search for forbidden patterns in resources..." ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt \ + ./library/ui-styles/src/main/res/values \ ./vector/src/main/res/color \ ./vector/src/main/res/layout \ ./vector/src/main/res/values \ diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 7362ff2d10..962a14843d 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -177,3 +177,6 @@ R\.string\.template_ ### Use the Clock interface, or use `measureTimeMillis` System\.currentTimeMillis\(\)===2 + +### Remove extra space between the name and the description +\* @\w+ \w+ + diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 322f29e5b7..a836edc47a 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -87,8 +87,7 @@ comments: EndOfSentenceFormat: active: true OutdatedDocumentation: - # TODO Enable it - active: false + active: true UndocumentedPublicClass: active: false UndocumentedPublicFunction: diff --git a/vector-config/build.gradle b/vector-config/build.gradle index 4156e74274..95b6a6215d 100644 --- a/vector-config/build.gradle +++ b/vector-config/build.gradle @@ -1,6 +1,5 @@ plugins { id 'com.android.library' - id 'org.jetbrains.kotlin.android' } android { @@ -14,7 +13,4 @@ android { sourceCompatibility versions.sourceCompat targetCompatibility versions.targetCompat } - kotlinOptions { - jvmTarget = "11" - } } diff --git a/vector/build.gradle b/vector/build.gradle index affe10e57b..0dd4ab297a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -31,7 +31,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 = 16 +ext.versionPatch = 18 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -370,7 +370,6 @@ dependencies { implementation libs.androidx.lifecycleProcess implementation libs.androidx.lifecycleRuntimeKtx - implementation libs.androidx.datastore implementation libs.androidx.datastorepreferences @@ -381,12 +380,11 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49' // FlowBinding implementation libs.github.flowBinding implementation libs.github.flowBindingAppcompat - implementation libs.github.flowBindingMaterial implementation libs.airbnb.epoxy implementation libs.airbnb.epoxyGlide @@ -452,9 +450,6 @@ dependencies { kapt libs.github.glideCompiler implementation 'com.github.yalantis:ucrop:2.2.8' - // Badge for compatibility - implementation 'me.leolin:ShortcutBadger:1.1.22@aar' - // Chat effects implementation 'nl.dionsegijn:konfetti-xml:2.0.2' @@ -510,9 +505,14 @@ dependencies { implementation 'commons-codec:commons-codec:1.15' // MapTiler - implementation 'org.maplibre.gl:android-sdk:9.5.2' - implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0' - + fdroidImplementation(libs.maplibre.androidSdk) { + exclude group: 'com.google.android.gms', module: 'play-services-location' + } + fdroidImplementation(libs.maplibre.pluginAnnotation) { + exclude group: 'com.google.android.gms', module: 'play-services-location' + } + gplayImplementation libs.maplibre.androidSdk + gplayImplementation libs.maplibre.pluginAnnotation // TESTS testImplementation libs.tests.junit diff --git a/vector/sampledata/live_location_users.json b/vector/sampledata/live_location_users.json new file mode 100644 index 0000000000..58d0fb5fa1 --- /dev/null +++ b/vector/sampledata/live_location_users.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "displayName": "Amandine", + "remainingTime": "9min left", + "lastUpdatedAt": "Updated 12min ago" + }, + { + "displayName": "You", + "remainingTime": "19min left", + "lastUpdatedAt": "Updated 1min ago" + } + ] +} diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt index 21fd226317..eaf39310a8 100644 --- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -165,7 +165,7 @@ class SecurityBootstrapTest : VerificationTestBase() { assert(uiSession.cryptoService().crossSigningService().isCrossSigningInitialized()) assert(uiSession.cryptoService().crossSigningService().canCrossSign()) assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown()) - assert(uiSession.cryptoService().keysBackupService().isEnabled) + assert(uiSession.cryptoService().keysBackupService().isEnabled()) assert(uiSession.cryptoService().keysBackupService().currentBackupVersion != null) assert(uiSession.sharedSecretStorageService().isRecoverySetup()) assert(uiSession.sharedSecretStorageService().isMegolmKeyInBackup()) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index 3c5de8b221..b9292bd916 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -120,6 +120,26 @@ class ElementRobot { .perform(ViewActions.closeSoftKeyboard(), click()) } } + // at this point we are in a race with the app restarting. The steps that happen are: + // - (initially) app has started, app has initial synched + // - (restart) app has strted, app has not initial synched + // - (racey) app shows some UI but overlays with initial sync ui + // - (initial sync finishes) app has started, has initial synched + + // We need to wait for the initial sync to complete; but we can't + // use waitForHome() like login does. + + // waitForHome() -- does not work because we have already fufilled the initialSync + // so we can racily have an IllegalStateException that we have transitioned from busy -> idle + // but never having sent the signal. + + // So we need to not start waiting for an initial sync until we have restarted + // then we do need to wait for the sync to complete. + + // Which is convoluted especially as it involves the app state refreshing + // so; in order to make this be more stable + // I hereby cheat and write: + Thread.sleep(30_000) } else -> { } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt index 4d35e3c550..289c6e21b4 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt @@ -104,11 +104,10 @@ class SpaceMenuRobot { fun leaveSpace() { clickOnSheet(R.id.leaveSpace) - waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton)) - clickOn(R.id.leave_selected) waitUntilActivityVisible { waitUntilViewVisible(ViewMatchers.withId(R.id.roomList)) } + clickOn(R.id.spaceLeaveSelectAll) clickOn(R.id.spaceLeaveButton) waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView)) } diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index 87aade0c8b..0b2b5cf90f 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -2,8 +2,6 @@ - - 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 f2904e4b1a..aa4df5e308 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 @@ -61,9 +61,9 @@ class DebugFeaturesStateFactory @Inject constructor( factory = VectorFeatures::isOnboardingCombinedRegisterEnabled ), createBooleanFeature( - label = "Live location sharing", - key = DebugFeatureKeys.liveLocationSharing, - factory = VectorFeatures::isLiveLocationEnabled + label = "FTUE Combined login", + key = DebugFeatureKeys.onboardingCombinedLogin, + factory = VectorFeatures::isOnboardingCombinedLoginEnabled ), ) ) 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 07fab8a58d..f36b1a804a 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 @@ -57,8 +57,8 @@ class DebugVectorFeatures( override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister) ?: vectorFeatures.isOnboardingCombinedRegisterEnabled() - override fun isLiveLocationEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing) - ?: vectorFeatures.isLiveLocationEnabled() + override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin) + ?: vectorFeatures.isOnboardingCombinedLoginEnabled() override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing) ?: vectorFeatures.isScreenSharingEnabled() @@ -116,6 +116,7 @@ object DebugFeatureKeys { val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel") val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize") val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register") + val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login") val liveLocationSharing = booleanPreferencesKey("live-location-sharing") val screenSharing = booleanPreferencesKey("screen-sharing") } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 425fd1081a..6e36d5dd81 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -46,7 +46,7 @@ object FcmHelper { * Store FCM token to the SharedPrefs * * @param context android context - * @param token the token to store + * @param token the token to store */ fun storeFcmToken(context: Context, token: String?) { // No op diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index b62520278a..a7d814052a 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -1,15 +1,12 @@ /* * Copyright 2019 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. @@ -32,7 +29,6 @@ import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager -import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils @@ -152,10 +148,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } - // update the badge counter - val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0 - BadgeProxy.updateBadgeCount(applicationContext, unreadCount) - val session = activeSessionHolder.getSafeActiveSession() if (session == null) { diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 3d44f10f76..74ab3b38f1 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -53,7 +53,7 @@ object FcmHelper { * TODO Store in realm * * @param context android context - * @param token the token to store + * @param token the token to store */ fun storeFcmToken(context: Context, token: String?) { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 20b7c4908a..2e31439e73 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -45,8 +45,6 @@ - - @@ -306,7 +304,9 @@ android:supportsPictureInPicture="true" /> - + + @@ -343,6 +343,7 @@ + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 0bead1f826..8f27776fbf 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -369,11 +369,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright 2012 Square, Inc. -
  • - ShortcutBadger -
    - Copyright 2014 Leo Lin -
  • diff-match-patch
    diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 1608d561bc..d44af53a55 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -72,6 +72,8 @@ class AppStateHandler @Inject constructor( val selectedRoomGroupingFlow = selectedSpaceDataSource.stream() + private val spaceBackstack = ArrayDeque() + fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? { // XXX we should somehow make it live :/ just a work around // For example just after creating a space and switching to it the @@ -87,12 +89,16 @@ class AppStateHandler @Inject constructor( } } - fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) { + fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) { + val currentSpace = (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.space() val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return - if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && - spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return + if (currentSpace != null && spaceId == currentSpace.roomId) return val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } + if (isForwardNavigation) { + spaceBackstack.addLast(currentSpace?.roomId) + } + if (persistNow) { uiStateRepository.storeGroupingMethod(true, uSession.sessionId) uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId) @@ -151,6 +157,8 @@ class AppStateHandler @Inject constructor( }.launchIn(session.coroutineScope) } + fun getSpaceBackstack() = spaceBackstack + fun safeActiveSpaceId(): String? { return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.spaceSummary?.roomId } 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 c68a35f4e5..e76f0ad672 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 @@ -101,8 +101,13 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedLoginFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedRegisterFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment +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.FtueAuthResetPasswordFragment @@ -474,6 +479,11 @@ interface FragmentModule { @FragmentKey(FtueAuthWaitForEmailFragment::class) fun bindFtueAuthWaitForEmailFragment(fragment: FtueAuthWaitForEmailFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthLegacyWaitForEmailFragment::class) + fun bindFtueAuthLegacyWaitForEmailFragment(fragment: FtueAuthLegacyWaitForEmailFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthWebFragment::class) @@ -494,6 +504,11 @@ interface FragmentModule { @FragmentKey(FtueAuthAccountCreatedFragment::class) fun bindFtueAuthAccountCreatedFragment(fragment: FtueAuthAccountCreatedFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthEmailEntryFragment::class) + fun bindFtueAuthEmailEntryFragment(fragment: FtueAuthEmailEntryFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthChooseDisplayNameFragment::class) @@ -509,6 +524,21 @@ interface FragmentModule { @FragmentKey(FtueAuthPersonalizationCompleteFragment::class) fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthCombinedLoginFragment::class) + fun bindFtueAuthCombinedLoginFragment(fragment: FtueAuthCombinedLoginFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(FtueAuthCombinedRegisterFragment::class) + fun bindFtueAuthCombinedRegisterFragment(fragment: FtueAuthCombinedRegisterFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(FtueAuthCombinedServerSelectionFragment::class) + fun bindFtueAuthCombinedServerSelectionFragment(fragment: FtueAuthCombinedServerSelectionFragment): Fragment + @Binds @IntoMap @FragmentKey(UserListFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 33afcf1dfb..313fd7b98c 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.location.LocationSharingViewModel +import im.vector.app.features.location.live.map.LocationLiveMapViewModel import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel @@ -600,4 +601,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(VectorAttachmentViewerViewModel::class) fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LocationLiveMapViewModel::class) + fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } 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 2945ae7d87..44f8bb1b3e 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 @@ -33,6 +33,7 @@ import im.vector.app.config.analyticsConfig import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.BuildMeta import im.vector.app.core.time.Clock import im.vector.app.core.time.DefaultClock import im.vector.app.features.analytics.AnalyticsConfig @@ -185,4 +186,8 @@ object VectorStaticModule { fun providesAnalyticsConfig(): AnalyticsConfig { return analyticsConfig } + + @Provides + @Singleton + fun providesBuildMeta() = BuildMeta() } diff --git a/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt index c43b2e4f09..8eaced1c48 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/UnrecognizedCertificateDialog.kt @@ -39,8 +39,9 @@ class UnrecognizedCertificateDialog @Inject constructor( * Display a certificate dialog box, asking the user about an unknown certificate * To use when user is currently logged in. * + * @param activity the Android activity * @param unrecognizedFingerprint the fingerprint for the unknown certificate - * @param callback callback to fire when the user makes a decision + * @param callback callback to fire when the user makes a decision */ fun show(activity: Activity, unrecognizedFingerprint: Fingerprint, @@ -80,9 +81,13 @@ class UnrecognizedCertificateDialog @Inject constructor( /** * Display a certificate dialog box, asking the user about an unknown certificate. * + * @param activity the Activity * @param unrecognizedFingerprint the fingerprint for the unknown certificate - * @param existing the current session already exist, so it mean that something has changed server side - * @param callback callback to fire when the user makes a decision + * @param existing the current session already exist, so it mean that something has changed server side + * @param callback callback to fire when the user makes a decision + * @param userId the matrix userId + * @param homeServerUrl the homeserver url + * @param homeServerConnectionConfigHasFingerprints true if the homeServerConnectionConfig has fingerprint */ private fun internalShow(activity: Activity, unrecognizedFingerprint: Fingerprint, diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 0f785e43a3..81844a403b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -16,9 +16,13 @@ package im.vector.app.core.extensions +import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.net.Uri +import android.os.Build import android.text.Spannable import android.text.SpannableString import android.text.style.ImageSpan @@ -27,11 +31,13 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import dagger.hilt.EntryPoints import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.resources.BuildMeta import java.io.OutputStream import kotlin.math.roundToInt @@ -77,3 +83,31 @@ val Context.dataStoreProvider: (String) -> DataStore by dataStorePr fun Context.safeOpenOutputStream(uri: Uri): OutputStream? { return contentResolver.openOutputStream(uri, "wt") } + +/** + * Checks for an active connection to infer if the device is offline. + * This is useful for breaking down UnknownHost exceptions and should not be used to determine if a valid connection is present + * + * @return true if no active connection is found + */ +@Suppress("deprecation") +@SuppressLint("NewApi") // false positive +fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean { + val connectivityManager = getSystemService()!! + return if (buildMeta.sdkInt > Build.VERSION_CODES.M) { + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + when { + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> false + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true -> false + else -> true + } + } else { + when (connectivityManager.activeNetworkInfo?.type) { + ConnectivityManager.TYPE_WIFI -> false + ConnectivityManager.TYPE_MOBILE -> false + ConnectivityManager.TYPE_VPN -> false + else -> true + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt index dfbd2eba97..61c4fe2174 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Fragment.kt @@ -36,9 +36,10 @@ fun Fragment.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): fun Fragment.addFragment( frameId: Int, fragment: Fragment, + tag: String? = null, allowStateLoss: Boolean = false ) { - parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) } + parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment, tag) } } fun Fragment.addFragment( diff --git a/vector/src/main/java/im/vector/app/core/extensions/Job.kt b/vector/src/main/java/im/vector/app/core/extensions/Job.kt new file mode 100644 index 0000000000..d9a4332ef2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Job.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +import kotlinx.coroutines.Job +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Property delegate for automatically cancelling the current job when setting a new value. + */ +fun cancelCurrentOnSet(): ReadWriteProperty = object : ReadWriteProperty { + private var currentJob: Job? = null + override fun getValue(thisRef: Any?, property: KProperty<*>): Job? = currentJob + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Job?) { + currentJob?.cancel() + currentJob = value + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index 1884909906..2a2198d96b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -66,7 +66,7 @@ fun Session.startSyncing(context: Context) { */ fun Session.hasUnsavedKeys(): Boolean { return cryptoService().inboundGroupSessionsCount(false) > 0 && - cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp + cryptoService().keysBackupService().getState() != KeysBackupState.ReadyToBackUp } fun Session.cannotLogoutSafely(): Boolean { diff --git a/vector/src/main/java/im/vector/app/core/extensions/String.kt b/vector/src/main/java/im/vector/app/core/extensions/String.kt new file mode 100644 index 0000000000..f035de469c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/String.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +private const val RTL_OVERRIDE_CHAR = '\u202E' +private const val LTR_OVERRIDE_CHAR = '\u202D' + +fun String.ensureEndsLeftToRight() = if (containsRtLOverride()) "$this$LTR_OVERRIDE_CHAR" else this + +fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR) + +fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR } 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 d9b92e78b7..205a0f40c4 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,7 +16,11 @@ package im.vector.app.core.extensions +import android.text.Editable +import android.view.View +import android.view.inputmethod.EditorInfo import com.google.android.material.textfield.TextInputLayout +import im.vector.app.core.platform.SimpleTextWatcher import kotlinx.coroutines.flow.map import reactivecircus.flowbinding.android.widget.textChanges @@ -30,3 +34,26 @@ fun TextInputLayout.hasSurroundingSpaces() = editText().text.toString().let { it fun TextInputLayout.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() } fun TextInputLayout.content() = editText().text.toString() + +fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() + +fun TextInputLayout.associateContentStateWith(button: View) { + editText().addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + val newContent = s.toString() + button.isEnabled = newContent.isNotEmpty() + } + }) +} + +fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) { + editText().setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + action() + true + } + else -> false + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt b/vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt index 142a7a6782..1d7247d758 100644 --- a/vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt +++ b/vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt @@ -29,9 +29,11 @@ sealed class ExternalIntentData { /** * Constructor for a text message. * - * @param text the text - * @param htmlText the HTML text - * @param format the formatted text format + * @property text the text + * @property htmlText the HTML text + * @property format the formatted text format + * @property clipDataItem the ClipData + * @property mimeType the mimetype */ data class IntentDataText( val text: CharSequence? = null, @@ -52,8 +54,8 @@ sealed class ExternalIntentData { /** * Constructor from a media Uri/. * - * @param uri the media uri - * @param filename the media file name + * @property uri the media uri + * @property filename the media file name */ data class IntentDataUri( val uri: Uri, diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index e68b5e1b07..38e304e1ce 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -27,6 +27,7 @@ import java.util.Locale * Returns the mimetype from a uri. * * @param context the context + * @param uri the uri * @return the mimetype */ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { 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 d904a5a4de..8a4aaa4b26 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 @@ -55,6 +55,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.dialogs.DialogLocker import im.vector.app.core.dialogs.UnrecognizedCertificateDialog +import im.vector.app.core.error.fatalError import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.observeNotNull import im.vector.app.core.extensions.registerStartForActivityResult @@ -611,11 +612,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver } }.show() } else { - if (vectorPreferences.failFast()) { - error("No CoordinatorLayout to display this snackbar!") - } else { - Timber.w("No CoordinatorLayout to display this snackbar!") - } + fatalError("No CoordinatorLayout to display this snackbar!", vectorPreferences.failFast()) } } diff --git a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt index dad7f26560..bea29195c9 100644 --- a/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/PushRulePreference.kt @@ -45,7 +45,7 @@ class PushRulePreference : VectorPreference { /** * Update the notification index. * - * @param pushRule + * @param notificationIndex the new notification index */ fun setIndex(notificationIndex: NotificationIndex?) { index = notificationIndex diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index b1bb4c7d88..c89dc7a73c 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -21,7 +21,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.session.pushers.HttpPusher import java.util.UUID import javax.inject.Inject import kotlin.math.abs @@ -55,7 +55,7 @@ class PushersManager @Inject constructor( currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) } - private fun createHttpPusher(pushKey: String) = PushersService.HttpPusher( + private fun createHttpPusher(pushKey: String) = HttpPusher( pushKey, stringProvider.getString(R.string.pusher_app_id), profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), diff --git a/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt b/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt new file mode 100644 index 0000000000..14d97e4c8f --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/resources/BuildMeta.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.resources + +import android.os.Build + +data class BuildMeta( + val sdkInt: Int = Build.VERSION.SDK_INT +) diff --git a/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt b/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt index 30cb1dcae4..6762bd68da 100644 --- a/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt @@ -19,27 +19,30 @@ package im.vector.app.core.resources import org.threeten.bp.Instant import org.threeten.bp.LocalDateTime import org.threeten.bp.ZoneId +import org.threeten.bp.ZoneOffset object DateProvider { - private val zoneId = ZoneId.systemDefault() - private val zoneOffset by lazy { - val now = currentLocalDateTime() - zoneId.rules.getOffset(now) - } + // recompute the zoneId each time we access it to handle change of timezones + private val defaultZoneId: ZoneId + get() = ZoneId.systemDefault() + + // recompute the zoneOffset each time we access it to handle change of timezones + private val defaultZoneOffset: ZoneOffset + get() = defaultZoneId.rules.getOffset(currentLocalDateTime()) fun toLocalDateTime(timestamp: Long?): LocalDateTime { val instant = Instant.ofEpochMilli(timestamp ?: 0) - return LocalDateTime.ofInstant(instant, zoneId) + return LocalDateTime.ofInstant(instant, defaultZoneId) } fun currentLocalDateTime(): LocalDateTime { val instant = Instant.now() - return LocalDateTime.ofInstant(instant, zoneId) + return LocalDateTime.ofInstant(instant, defaultZoneId) } fun toTimestamp(localDateTime: LocalDateTime): Long { - return localDateTime.toInstant(zoneOffset).toEpochMilli() + return localDateTime.toInstant(defaultZoneOffset).toEpochMilli() } } diff --git a/vector/src/main/java/im/vector/app/core/resources/Resource.kt b/vector/src/main/java/im/vector/app/core/resources/Resource.kt index f14c9b834d..861dfdb781 100644 --- a/vector/src/main/java/im/vector/app/core/resources/Resource.kt +++ b/vector/src/main/java/im/vector/app/core/resources/Resource.kt @@ -56,8 +56,8 @@ data class Resource( /** * Get a resource stream and metadata about it given its URI returned from onActivityResult. * - * @param context the context. - * @param uri the URI + * @param context the context. + * @param uri the URI * @param providedMimetype the mimetype * @return a [Resource] encapsulating the opened resource stream and associated metadata * or `null` if opening the resource stream failed. diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index d39dcbe318..09bb78c926 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -56,4 +56,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun showLiveSenderInfo(): Boolean { return vectorPreferences.showLiveSenderInfo() } + + fun autoplayAnimatedImages(): Boolean { + return vectorPreferences.autoplayAnimatedImages() + } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt index f2ea79984e..80603aa3bf 100755 --- a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt @@ -54,6 +54,7 @@ class KeysBackupBanner @JvmOverloads constructor( * This methods is responsible for rendering the view according to the newState. * * @param newState the newState representing the view + * @param force true to force the rendering of the view */ fun render(newState: State, force: Boolean = false) { if (newState == state && !force) { diff --git a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 10ab0fc027..9f3e6a91cf 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -30,7 +30,7 @@ import me.gujun.android.span.span /** * Open a web view above the current activity. * - * @param url the url to open + * @param url the url to open */ fun Context.displayInWebView(url: String) { val wv = WebView(this) diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt index d6a63dca10..3e82ecd5f2 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt @@ -16,7 +16,7 @@ package im.vector.app.core.utils -import com.vanniktech.emoji.EmojiUtils +import com.vanniktech.emoji.isOnlyEmojis /** * Test if a string contains emojis. @@ -28,7 +28,7 @@ import com.vanniktech.emoji.EmojiUtils */ fun containsOnlyEmojis(str: String?): Boolean { // Now rely on vanniktech library - return EmojiUtils.isOnlyEmojis(str) + return str.isOnlyEmojis() } /** diff --git a/vector/src/main/java/im/vector/app/core/utils/EvenBetterLinkMovementMethod.kt b/vector/src/main/java/im/vector/app/core/utils/EvenBetterLinkMovementMethod.kt index b9c1386933..a53c8161b1 100644 --- a/vector/src/main/java/im/vector/app/core/utils/EvenBetterLinkMovementMethod.kt +++ b/vector/src/main/java/im/vector/app/core/utils/EvenBetterLinkMovementMethod.kt @@ -26,9 +26,9 @@ class EvenBetterLinkMovementMethod(private val onLinkClickListener: OnLinkClickL interface OnLinkClickListener { /** - * @param textView The TextView on which a click was registered. - * @param span The ClickableSpan which is clicked on. - * @param url The clicked URL. + * @param textView The TextView on which a click was registered. + * @param span The ClickableSpan which is clicked on. + * @param url The clicked URL. * @param actualText The original text which is spanned. Can be used to compare actualText and target url to prevent misleading urls. * @return true if this click was handled, false to let Android handle the URL. */ 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 8bfbcaeb92..9616e35840 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 @@ -170,9 +170,9 @@ fun openUri(activity: Activity, uri: String) { /** * Send media to a third party application. * - * @param activity the activity + * @param activity the activity * @param savedMediaPath the media path - * @param mimeType the media mime type. + * @param mimeType the media mime type. */ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { val file = File(savedMediaPath) @@ -415,8 +415,8 @@ fun selectTxtFileToWrite( * * ~~ This is copied from the old matrix sdk ~~ * - * @param sourceFile the file source path - * @param dstDirPath the dst path + * @param sourceFile the file source path + * @param dstDirPath the dst path * @param outputFilename optional the output filename * @param currentTimeMillis the current time in milliseconds * @return the created file diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index b4f8de2485..705f683d2e 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,7 +19,6 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager -import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -43,11 +42,6 @@ val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) -val PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION) -} else { - PERMISSIONS_EMPTY -} // This is not ideal to store the value like that, but it works private var permissionDialogDisplayed = false @@ -101,9 +95,9 @@ private fun onPermissionResult(result: Map, lambda: (allGranted * explain why vector needs the corresponding permission. * * @param permissionsToBeGranted the permissions to be granted - * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param activity the calling Activity that is requesting the permissions (or fragment parent) * @param activityResultLauncher from the calling fragment/Activity that is requesting the permissions - * @param rationaleMessage message to be displayed BEFORE requesting for the permission + * @param rationaleMessage message to be displayed BEFORE requesting for the permission * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow) */ fun checkPermissions(permissionsToBeGranted: List, @@ -145,7 +139,7 @@ fun checkPermissions(permissionsToBeGranted: List, * To be call after the permission request. * * @param permissionsToBeGranted the permissions to be granted - * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param activity the calling Activity that is requesting the permissions (or fragment parent) * * @return true if one of the permission has been denied and the user check the do not ask again checkbox */ diff --git a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt index a0fd3addac..bbed2f6000 100644 --- a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt @@ -90,6 +90,7 @@ fun getCallRingtoneName(context: Context): String? { /** * Sets the selected ringtone for riot calls. * + * @param context Android context * @param ringtoneUri * @see Ringtone */ diff --git a/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt index 69702fc793..aa1917e326 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt @@ -22,6 +22,7 @@ import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import androidx.annotation.ColorInt import me.gujun.android.span.Span +import me.gujun.android.span.span fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { if (match.isEmpty()) return this @@ -56,3 +57,17 @@ fun Span.bullet(text: CharSequence = "", build() }) } + +fun String.colorTerminatingFullStop(@ColorInt color: Int): CharSequence { + val fullStop = "." + return if (endsWith(fullStop)) { + span { + +this@colorTerminatingFullStop.removeSuffix(fullStop) + span(fullStop) { + textColor = color + } + } + } else { + this + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 18a467d8d0..1d9ac6c3ef 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -82,7 +82,9 @@ fun requestDisablingBatteryOptimization(activity: Activity, activityResultLaunch * Copy a text to the clipboard, and display a Toast when done. * * @param context the context - * @param text the text to copy + * @param text the text to copy + * @param showToast true to also show a Toast to the user + * @param toastMessage content of the toast message as a String resource */ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage: Int = R.string.copied_to_clipboard) { val clipboard = context.getSystemService()!! diff --git a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt index fb386e0876..bd1e396126 100644 --- a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt +++ b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt @@ -23,7 +23,8 @@ const val THREE_MINUTES = 3 * 60_000L /** * Store an object T for a specific period of time. - * @param delay delay to keep the data, in millis + * @param T type of the data to store + * @property delay delay to keep the data, in millis */ open class TemporaryStore(private val delay: Long = THREE_MINUTES) { diff --git a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt index 992a85679c..d6af3f5afb 100644 --- a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt @@ -19,11 +19,16 @@ package im.vector.app.core.utils import android.content.Context import android.os.Build import android.text.format.Formatter +import im.vector.app.R +import im.vector.app.core.resources.StringProvider import org.threeten.bp.Duration import java.util.TreeMap object TextUtils { + private const val MINUTES_PER_HOUR = 60 + private const val SECONDS_PER_MINUTE = 60 + private val suffixes = TreeMap().also { it[1000] = "k" it[1000000] = "M" @@ -71,13 +76,79 @@ object TextUtils { } fun formatDuration(duration: Duration): String { - val hours = duration.seconds / 3600 - val minutes = (duration.seconds % 3600) / 60 - val seconds = duration.seconds % 60 + val hours = getHours(duration) + val minutes = getMinutes(duration) + val seconds = getSeconds(duration) return if (hours > 0) { String.format("%d:%02d:%02d", hours, minutes, seconds) } else { String.format("%02d:%02d", minutes, seconds) } } + + fun formatDurationWithUnits(context: Context, duration: Duration, appendSeconds: Boolean = true): String { + return formatDurationWithUnits(duration, context::getString, appendSeconds) + } + + fun formatDurationWithUnits(stringProvider: StringProvider, duration: Duration, appendSeconds: Boolean = true): String { + return formatDurationWithUnits(duration, stringProvider::getString, appendSeconds) + } + + /** + * We don't always have Context to get strings or we want to use StringProvider instead. + * So we can pass the getString function either from Context or the StringProvider. + * @param duration duration to be formatted + * @param getString getString method from Context or StringProvider + * @param appendSeconds if false than formatter will not append seconds + * @return formatted duration with a localized form like "10h 30min 5sec" + */ + private fun formatDurationWithUnits(duration: Duration, getString: ((Int) -> String), appendSeconds: Boolean = true): String { + val hours = getHours(duration) + val minutes = getMinutes(duration) + val seconds = getSeconds(duration) + val builder = StringBuilder() + when { + hours > 0 -> { + appendHours(getString, builder, hours) + if (minutes > 0) { + builder.append(" ") + appendMinutes(getString, builder, minutes) + } + if (appendSeconds && seconds > 0) { + builder.append(" ") + appendSeconds(getString, builder, seconds) + } + } + minutes > 0 -> { + appendMinutes(getString, builder, minutes) + if (appendSeconds && seconds > 0) { + builder.append(" ") + appendSeconds(getString, builder, seconds) + } + } + else -> { + appendSeconds(getString, builder, seconds) + } + } + return builder.toString() + } + + private fun appendHours(getString: ((Int) -> String), builder: StringBuilder, hours: Int) { + builder.append(hours) + builder.append(getString(R.string.time_unit_hour_short)) + } + + private fun appendMinutes(getString: ((Int) -> String), builder: StringBuilder, minutes: Int) { + builder.append(minutes) + builder.append(getString(R.string.time_unit_minute_short)) + } + + private fun appendSeconds(getString: ((Int) -> String), builder: StringBuilder, seconds: Int) { + builder.append(seconds) + builder.append(getString(R.string.time_unit_second_short)) + } + + private fun getHours(duration: Duration): Int = duration.toHours().toInt() + private fun getMinutes(duration: Duration): Int = duration.toMinutes().toInt() % MINUTES_PER_HOUR + private fun getSeconds(duration: Duration): Int = (duration.seconds % SECONDS_PER_MINUTE).toInt() } diff --git a/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt new file mode 100644 index 0000000000..c829313256 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.appbar.AppBarLayout + +/** + * [AppBarLayout.Behavior] subclass with a possibility to disable behavior. + * Useful for cases when in some view state we want prevent toolbar from collapsing/expanding by scroll events + */ +class ToggleableAppBarLayoutBehavior : AppBarLayout.Behavior { + constructor() : super() + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + var isEnabled = true + + override fun onStartNestedScroll(parent: CoordinatorLayout, + child: AppBarLayout, + directTargetChild: View, + target: View, + nestedScrollAxes: Int, + type: Int): Boolean { + return isEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type) + } + + override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, + child: AppBarLayout, + target: View, + dxConsumed: Int, + dyConsumed: Int, + dxUnconsumed: Int, + dyUnconsumed: Int, + type: Int, + consumed: IntArray) { + if (!isEnabled) return + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) + } + + override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, + child: AppBarLayout, + target: View, + dx: Int, + dy: Int, + consumed: IntArray, + type: Int) { + if (!isEnabled) return + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) + } +} 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 42693a53f9..6a7a0865de 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -26,7 +26,7 @@ interface VectorFeatures { fun isOnboardingUseCaseEnabled(): Boolean fun isOnboardingPersonalizeEnabled(): Boolean fun isOnboardingCombinedRegisterEnabled(): Boolean - fun isLiveLocationEnabled(): Boolean + fun isOnboardingCombinedLoginEnabled(): Boolean fun isScreenSharingEnabled(): Boolean enum class OnboardingVariant { @@ -43,6 +43,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false - override fun isLiveLocationEnabled(): Boolean = false + override fun isOnboardingCombinedLoginEnabled() = false override fun isScreenSharingEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt b/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt deleted file mode 100644 index c15ec8bdf4..0000000000 --- a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2019 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. - */ - -@file:Suppress("UNUSED_PARAMETER") - -package im.vector.app.features.badge - -import android.content.Context -import android.os.Build -import me.leolin.shortcutbadger.ShortcutBadger -import org.matrix.android.sdk.api.session.Session - -/** - * Manage application badge (displayed in the launcher). - */ -object BadgeProxy { - - /** - * Badge is now managed by notification channel, so no need to use compatibility library in recent versions. - * - * @return true if library ShortcutBadger can be used - */ - private fun useShortcutBadger() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O - - /** - * Update the application badge value. - * - * @param context the context - * @param badgeValue the new badge value - */ - fun updateBadgeCount(context: Context, badgeValue: Int) { - if (!useShortcutBadger()) { - return - } - - ShortcutBadger.applyCount(context, badgeValue) - } - - /** - * Refresh the badge count for specific configurations.

    - * The refresh is only effective if the device is: - * * offline * does not support FCM - * * FCM registration failed - *

    Notifications rooms are parsed to track the notification count value. - * - * @param aSession session value - * @param aContext App context - */ - fun specificUpdateBadgeUnreadCount(aSession: Session?, aContext: Context?) { - if (!useShortcutBadger()) { - return - } - - /* TODO - val dataHandler: MXDataHandler - - // sanity check - if (null == aContext || null == aSession) { - Timber.w("## specificUpdateBadgeUnreadCount(): invalid input null values") - } else { - dataHandler = aSession.dataHandler - - if (dataHandler == null) { - Timber.w("## specificUpdateBadgeUnreadCount(): invalid DataHandler instance") - } else { - if (aSession.isAlive) { - var isRefreshRequired: Boolean - val pushManager = Matrix.getInstance(aContext)!!.pushManager - - // update the badge count if the device is offline, FCM is not supported or FCM registration failed - isRefreshRequired = !Matrix.getInstance(aContext)!!.isConnected - isRefreshRequired = isRefreshRequired or (null != pushManager && (!pushManager.useFcm() || !pushManager.hasRegistrationToken())) - - if (isRefreshRequired) { - updateBadgeCount(aContext, dataHandler) - } - } - } - } - */ - } - - /** - * Update the badge count value according to the rooms content. - * - * @param aContext App context - * @param aDataHandler data handler instance - */ - private fun updateBadgeCount(aSession: Session?, aContext: Context?) { - if (!useShortcutBadger()) { - return - } - - /* TODO - //sanity check - if (null == aContext || null == aDataHandler) { - Timber.w("## updateBadgeCount(): invalid input null values") - } else if (null == aDataHandler.store) { - Timber.w("## updateBadgeCount(): invalid store instance") - } else { - val roomCompleteList = ArrayList(aDataHandler.store.rooms) - var unreadRoomsCount = 0 - - for (room in roomCompleteList) { - if (room.notificationCount > 0) { - unreadRoomsCount++ - } - } - - // update the badge counter - Timber.v("## updateBadgeCount(): badge update count=$unreadRoomsCount") - updateBadgeCount(aContext, unreadRoomsCount) - } - */ - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt index d4793640d3..6577d0374d 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -122,6 +122,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un * Updates the audio route for the given mode. * * @param mode the audio mode to be used when computing the audio route. + * @param force true to force setting the audio route * @return `true` if the audio route was updated successfully; * `false`, otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 49e35687f4..17b8087601 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -30,7 +30,8 @@ class CommandParser @Inject constructor() { /** * Convert the text message into a Slash command. * - * @param textMessage the text message + * @param textMessage the text message + * @param isInThreadTimeline true if the user is currently typing in a thread * @return a parsed slash command (ok or error) */ fun parseSlashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand { @@ -412,8 +413,8 @@ class CommandParser @Inject constructor() { /** * Checks whether or not the current command is not supported by threads. - * @param slashCommand the slash command that will be checked * @param isInThreadTimeline if its true we are in a thread timeline + * @param slashCommand the slash command that will be checked * @return The command that is not supported */ private fun getNotSupportedByThreads(isInThreadTimeline: Boolean, slashCommand: String): Command? { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index b47b84af71..dea481dbad 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -60,7 +60,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS init { setState { this.copy( - keysBackupState = keysBackupService.state, + keysBackupState = keysBackupService.getState(), keysBackupVersion = keysBackupService.keysBackupVersion ) } @@ -206,7 +206,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS } fun canExit(): Boolean { - val currentBackupState = keysBackupService.state + val currentBackupState = keysBackupService.getState() return currentBackupState == KeysBackupState.Unknown || currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index e045ac020d..649890cda9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfo import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec -import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.util.toBase64NoPadding import org.matrix.android.sdk.flow.flow import timber.log.Timber @@ -283,7 +283,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( session.sharedSecretStorageService().storeSecret( name = name, secretBase64 = value, - keys = listOf(SharedSecretStorageService.KeyRef(keyInfo.id, keySpec)) + keys = listOf(KeyRef(keyInfo.id, keySpec)) ) decryptedSecretMap[name] = value } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt index e620500d70..5d866c7220 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -26,8 +26,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec -import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.toBase64NoPadding @@ -142,7 +142,7 @@ class BackupToQuadSMigrationTask @Inject constructor( quadS.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, curveKey.toBase64NoPadding(), - listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)) + listOf(KeyRef(info.keyId, info.keySpec)) ) // save for gossiping diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index c26aec45dd..c7c367f5ec 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -34,7 +34,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner -import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.api.util.awaitCallback @@ -67,6 +67,7 @@ data class Params( val progressListener: BootstrapProgressListener? = null, val passphrase: String?, val keySpec: SsssKeySpec? = null, + val forceResetIfSomeSecretsAreMissing: Boolean = false, val setupMode: SetupMode ) @@ -83,6 +84,7 @@ class BootstrapCrossSigningTask @Inject constructor( // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() || + (params.forceResetIfSomeSecretsAreMissing && !crossSigningService.allPrivateKeysKnown()) || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) || (params.setupMode == SetupMode.HARD_RESET) if (shouldSetCrossSigning) { @@ -183,7 +185,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( MASTER_KEY_SSSS_NAME, mskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) params.progressListener?.onProgress( WaitingViewData( @@ -195,7 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( USER_SIGNING_KEY_SSSS_NAME, uskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) params.progressListener?.onProgress( WaitingViewData( @@ -206,7 +208,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, sskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) } catch (failure: Failure) { Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") @@ -258,7 +260,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, secret, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) } } else { @@ -275,7 +277,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, secret, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) } } else { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt index 3be2f020b8..3d078a82ed 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt @@ -26,6 +26,7 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapSetupRecoveryBinding +import im.vector.app.features.raw.wellknown.SecureBackupMethod import javax.inject.Inject class BootstrapSetupRecoveryKeyFragment @Inject constructor() : @@ -55,27 +56,40 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() : } override fun invalidate() = withState(sharedViewModel) { state -> - if (state.step is BootstrapStep.FirstForm) { - if (state.step.keyBackUpExist) { - // Display the set up action - views.bootstrapSetupSecureSubmit.isVisible = true - views.bootstrapSetupSecureUseSecurityKey.isVisible = false - views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = false - views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false - } else { - if (state.step.reset) { - views.bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title) - views.bootstrapSetupWarningTextView.isVisible = true - } else { - views.bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle) - views.bootstrapSetupWarningTextView.isVisible = false - } - // Choose between create a passphrase or use a recovery key - views.bootstrapSetupSecureSubmit.isVisible = false - views.bootstrapSetupSecureUseSecurityKey.isVisible = true - views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = true - views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true - } + val firstFormStep = state.step as? BootstrapStep.FirstForm ?: return@withState + + if (firstFormStep.keyBackUpExist) { + renderStateWithExistingKeyBackup() + } else { + renderSetupHeader(needsReset = firstFormStep.reset) + views.bootstrapSetupSecureSubmit.isVisible = false + + // Choose between create a passphrase or use a recovery key + renderBackupMethodActions(firstFormStep.methods) } } + + private fun renderStateWithExistingKeyBackup() = with(views) { + // Display the set up action + bootstrapSetupSecureSubmit.isVisible = true + // Disable creating backup / passphrase options + bootstrapSetupSecureUseSecurityKey.isVisible = false + bootstrapSetupSecureUseSecurityPassphrase.isVisible = false + bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false + } + + private fun renderSetupHeader(needsReset: Boolean) = with(views) { + bootstrapSetupSecureText.text = if (needsReset) { + getString(R.string.reset_secure_backup_title) + } else { + getString(R.string.bottom_sheet_setup_secure_backup_subtitle) + } + bootstrapSetupWarningTextView.isVisible = needsReset + } + + private fun renderBackupMethodActions(method: SecureBackupMethod) = with(views) { + bootstrapSetupSecureUseSecurityKey.isVisible = method.isKeyAvailable + bootstrapSetupSecureUseSecurityPassphrase.isVisible = method.isPassphraseAvailable + bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = method.isPassphraseAvailable + } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index b67970e61f..15ea90ae0a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -33,6 +33,10 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.raw.wellknown.SecureBackupMethod +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isSecureBackupRequired +import im.vector.app.features.raw.wellknown.secureBackupMethod import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth @@ -41,7 +45,9 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult @@ -61,6 +67,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, private val session: Session, + private val rawService: RawService, private val bootstrapTask: BootstrapCrossSigningTask, private val migrationTask: BackupToQuadSMigrationTask, ) : VectorViewModel(initialState) { @@ -83,12 +90,33 @@ class BootstrapSharedViewModel @AssistedInject constructor( init { + setState { + copy(step = BootstrapStep.CheckingMigration, isRecoverySetup = session.sharedSecretStorageService().isRecoverySetup()) + } + + // Refresh the well-known configuration + viewModelScope.launch(Dispatchers.IO) { + val wellKnown = rawService.getElementWellknown(session.sessionParams) + setState { + copy( + isSecureBackupRequired = wellKnown?.isSecureBackupRequired().orFalse(), + secureBackupMethod = wellKnown?.secureBackupMethod() ?: SecureBackupMethod.KEY_OR_PASSPHRASE, + ) + } + } + when (initialState.setupMode) { SetupMode.PASSPHRASE_RESET, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET, SetupMode.HARD_RESET -> { setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true)) + copy( + step = BootstrapStep.FirstForm( + keyBackUpExist = false, + reset = session.sharedSecretStorageService().isRecoverySetup(), + methods = this.secureBackupMethod + ) + ) } } SetupMode.CROSS_SIGNING_ONLY -> { @@ -112,7 +140,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( // we just resume plain bootstrap doesKeyBackupExist = false setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) + copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) } } else { // we need to get existing backup passphrase/key and convert to SSSS @@ -126,7 +154,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( doesKeyBackupExist = true isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) + copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) } } } @@ -411,6 +439,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( progressListener = progressListener, passphrase = state.passphrase, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, + forceResetIfSomeSecretsAreMissing = state.isSecureBackupRequired, setupMode = state.setupMode ) ) { bootstrapResult -> @@ -419,14 +448,22 @@ class BootstrapSharedViewModel @AssistedInject constructor( _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } is BootstrapResult.Success -> { - setState { - copy( - recoveryKeyCreationInfo = bootstrapResult.keyInfo, - step = BootstrapStep.SaveRecoveryKey( - // If a passphrase was used, saving key is optional - state.passphrase != null - ) - ) + val isSecureBackupRequired = state.isSecureBackupRequired + val secureBackupMethod = state.secureBackupMethod + + if (state.passphrase != null && isSecureBackupRequired && secureBackupMethod == SecureBackupMethod.PASSPHRASE) { + // Go straight to conclusion, skip the save key step + _viewEvents.post(BootstrapViewEvents.Dismiss(success = true)) + } else { + setState { + copy( + recoveryKeyCreationInfo = bootstrapResult.keyInfo, + step = BootstrapStep.SaveRecoveryKey( + // If a passphrase was used, saving key is optional + state.passphrase != null + ) + ) + } } } is BootstrapResult.InvalidPasswordError -> { @@ -476,7 +513,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } else { setState { copy( - step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), + step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod), // Also reset the passphrase passphrase = null, passphraseRepeat = null, @@ -489,7 +526,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( is BootstrapStep.SetupPassphrase -> { setState { copy( - step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), + step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod), // Also reset the passphrase passphrase = null, passphraseRepeat = null @@ -504,11 +541,25 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapStep.AccountReAuth -> { - _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + if (state.canLeave) { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } else { + // Go back to the first step + setState { + copy( + step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod), + // Also reset the passphrase + passphrase = null, + passphraseRepeat = null + ) + } + } } BootstrapStep.Initializing -> { // do we let you cancel from here? - _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + if (state.canLeave) { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } } is BootstrapStep.SaveRecoveryKey, BootstrapStep.DoneSuccess -> { @@ -516,18 +567,20 @@ class BootstrapSharedViewModel @AssistedInject constructor( } BootstrapStep.CheckingMigration -> Unit is BootstrapStep.FirstForm -> { - _viewEvents.post( - when (state.setupMode) { - SetupMode.CROSS_SIGNING_ONLY, - SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap() - else -> BootstrapViewEvents.Dismiss(success = false) - } - ) + if (state.canLeave) { + _viewEvents.post( + when (state.setupMode) { + SetupMode.CROSS_SIGNING_ONLY, + SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap() + else -> BootstrapViewEvents.Dismiss(success = false) + } + ) + } } is BootstrapStep.GetBackupSecretForMigration -> { setState { copy( - step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), + step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod), // Also reset the passphrase passphrase = null, passphraseRepeat = null, @@ -549,3 +602,5 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } } + +private val BootstrapViewState.canLeave: Boolean get() = !isSecureBackupRequired || isRecoverySetup diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index a4fa31ad03..3fb20ccf9f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -16,6 +16,8 @@ package im.vector.app.features.crypto.recover +import im.vector.app.features.raw.wellknown.SecureBackupMethod + /** * TODO The schema is not up to date * @@ -89,7 +91,7 @@ sealed class BootstrapStep { object CheckingMigration : BootstrapStep() // Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists - data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep() + data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false, val methods: SecureBackupMethod) : BootstrapStep() object SetupPassphrase : BootstrapStep() object ConfirmPassphrase : BootstrapStep() diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt index 9d5760cbf9..2d27c165e6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import com.nulabinc.zxcvbn.Strength import im.vector.app.core.platform.WaitingViewData +import im.vector.app.features.raw.wellknown.SecureBackupMethod import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo data class BootstrapViewState( @@ -34,7 +35,10 @@ data class BootstrapViewState( val passphraseConfirmMatch: Async = Uninitialized, val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, val initializationWaitingViewData: WaitingViewData? = null, - val recoverySaveFileProcess: Async = Uninitialized + val recoverySaveFileProcess: Async = Uninitialized, + val isSecureBackupRequired: Boolean = false, + val secureBackupMethod: SecureBackupMethod = SecureBackupMethod.KEY_OR_PASSPHRASE, + val isRecoverySetup: Boolean = true ) : MavericksState { constructor(args: BootstrapBottomSheet.Args) : this(setupMode = args.setUpMode) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index a5142ad8bf..c4ae2d278b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -27,7 +27,7 @@ sealed class VerificationAction : VectorViewModelAction { object OtherUserDidNotScanned : VerificationAction() data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() - object GotItConclusion : VerificationAction() + data class GotItConclusion(val verified: Boolean) : VerificationAction() object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index b65ce4b30b..a7fd166076 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -30,9 +30,13 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isSecureBackupRequired import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -78,6 +82,7 @@ data class VerificationBottomSheetViewState( val userWantsToCancel: Boolean = false, val userThinkItsNotHim: Boolean = false, val quadSContainsSecrets: Boolean = true, + val isVerificationRequired: Boolean = false, val quadSHasBeenReset: Boolean = false, val hasAnyOtherSession: Boolean = false ) : MavericksState { @@ -92,6 +97,7 @@ data class VerificationBottomSheetViewState( class VerificationBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: VerificationBottomSheetViewState, + private val rawService: RawService, private val session: Session, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stringProvider: StringProvider) : @@ -108,6 +114,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( init { session.cryptoService().verificationService().addListener(this) + // This is async, but at this point should be in cache + // so it's ok to not wait until result + viewModelScope.launch(Dispatchers.IO) { + val wellKnown = rawService.getElementWellknown(session.sessionParams) + setState { + copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse()) + } + } + val userItem = session.getUser(initialState.otherUserId) var autoReady = false @@ -182,8 +197,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( state.verifyingFrom4S) { // you cannot cancel anymore } else { - setState { - copy(userWantsToCancel = true) + if (!state.isVerificationRequired) { + setState { + copy(userWantsToCancel = true) + } } } } @@ -341,7 +358,18 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( ?.shortCodeDoesNotMatch() } is VerificationAction.GotItConclusion -> { - _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + if (state.isVerificationRequired && !action.verified) { + // we should go back to first screen + setState { + copy( + pendingRequest = Uninitialized, + sasTransactionState = null, + qrTransactionState = null + ) + } + } else { + _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + } } is VerificationAction.SkipVerification -> { _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt index 7f6678a73c..9203b8ab0a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -84,7 +84,7 @@ class VerificationConclusionController @Inject constructor( notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence()) } - bottomDone() + bottomGotIt() } ConclusionState.CANCELLED -> { bottomSheetVerificationNoticeItem { @@ -92,18 +92,7 @@ class VerificationConclusionController @Inject constructor( notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence()) } - bottomSheetDividerItem { - id("sep0") - } - - bottomSheetVerificationActionItem { - id("got_it") - title(host.stringProvider.getString(R.string.sas_got_it)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - iconRes(R.drawable.ic_arrow_right) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - listener { host.listener?.onButtonTapped() } - } + bottomGotIt() } } } @@ -120,11 +109,27 @@ class VerificationConclusionController @Inject constructor( titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconRes(R.drawable.ic_arrow_right) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - listener { host.listener?.onButtonTapped() } + listener { host.listener?.onButtonTapped(true) } + } + } + + private fun bottomGotIt() { + val host = this + bottomSheetDividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("got_it") + title(host.stringProvider.getString(R.string.sas_got_it)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { host.listener?.onButtonTapped(false) } } } interface Listener { - fun onButtonTapped() + fun onButtonTapped(success: Boolean) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt index f45bc3d44e..85b90e6004 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionFragment.kt @@ -73,7 +73,7 @@ class VerificationConclusionFragment @Inject constructor( controller.update(state) } - override fun onButtonTapped() { - sharedViewModel.handle(VerificationAction.GotItConclusion) + override fun onButtonTapped(success: Boolean) { + sharedViewModel.handle(VerificationAction.GotItConclusion(success)) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index 781677433b..3c7b4ffebd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -88,17 +88,19 @@ class VerificationRequestController @Inject constructor( } } - bottomSheetDividerItem { - id("sep1") - } + if (!state.isVerificationRequired) { + bottomSheetDividerItem { + id("sep1") + } - bottomSheetVerificationActionItem { - id("skip") - title(host.stringProvider.getString(R.string.action_skip)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) - iconRes(R.drawable.ic_arrow_right) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) - listener { host.listener?.onClickSkip() } + bottomSheetVerificationActionItem { + id("skip") + title(host.stringProvider.getString(R.string.action_skip)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + listener { host.listener?.onClickSkip() } + } } } else { val styledText = 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 ee9340f37d..4ddd082f49 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 @@ -50,6 +50,7 @@ import im.vector.app.features.MainActivityArgs import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.ViewRoom +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.OriginOfMatrixTo @@ -199,43 +200,13 @@ class HomeActivity : when (sharedAction) { is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { - views.drawerLayout.closeDrawer(GravityCompat.START) - - // Temporary - // When switching from space to group or group to space, we need to reload the fragment - // To be removed when dropping legacy groups - if (sharedAction.clearFragment) { - replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) - } else { - // nop - } - // we might want to delay that to avoid having the drawer animation lagging - // would be probably better to let the drawer do that? in the on closed callback? - } - is HomeActivitySharedAction.OpenSpacePreview -> { - startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) - } - is HomeActivitySharedAction.AddSpace -> { - createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) - } - is HomeActivitySharedAction.ShowSpaceSettings -> { - // open bottom sheet - SpaceSettingsMenuBottomSheet - .newInstance(sharedAction.spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener { - override fun onShareSpaceSelected(spaceId: String) { - ShareSpaceBottomSheet.show(supportFragmentManager, spaceId) - } - }) - .show(supportFragmentManager, "SPACE_SETTINGS") - } - is HomeActivitySharedAction.OpenSpaceInvite -> { - SpaceInviteBottomSheet.newInstance(sharedAction.spaceId) - .show(supportFragmentManager, "SPACE_INVITE") - } - HomeActivitySharedAction.SendSpaceFeedBack -> { - bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) - } + is HomeActivitySharedAction.OpenGroup -> openGroup(sharedAction.shouldClearFragment) + is HomeActivitySharedAction.OpenSpacePreview -> startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) + is HomeActivitySharedAction.AddSpace -> createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) + is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId) + is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId) + HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) + HomeActivitySharedAction.CloseGroup -> closeGroup() } } .launchIn(lifecycleScope) @@ -256,6 +227,14 @@ class HomeActivity : is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() + HomeActivityViewEvents.StartRecoverySetupFlow -> handleStartRecoverySetup() + is HomeActivityViewEvents.ForceVerification -> { + if (it.sendRequest) { + navigator.requestSelfSessionVerification(this) + } else { + navigator.waitSessionVerification(this) + } + } is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() @@ -272,6 +251,37 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } + private fun openGroup(shouldClearFragment: Boolean) { + views.drawerLayout.closeDrawer(GravityCompat.START) + + // When switching from space to group or group to space, we need to reload the fragment + if (shouldClearFragment) { + replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) + } else { + // do nothing + } + } + + private fun showSpaceSettings(spaceId: String) { + // open bottom sheet + SpaceSettingsMenuBottomSheet + .newInstance(spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener { + override fun onShareSpaceSelected(spaceId: String) { + ShareSpaceBottomSheet.show(supportFragmentManager, spaceId) + } + }) + .show(supportFragmentManager, "SPACE_SETTINGS") + } + + private fun openSpaceInvite(spaceId: String) { + SpaceInviteBottomSheet.newInstance(spaceId) + .show(supportFragmentManager, "SPACE_INVITE") + } + + private fun closeGroup() { + views.drawerLayout.openDrawer(GravityCompat.START) + } + private fun handleShowAnalyticsOptIn() { navigator.openAnalyticsOptIn(this) } @@ -354,6 +364,13 @@ class HomeActivity : } } + private fun handleStartRecoverySetup() { + // To avoid IllegalStateException in case the transaction was executed after onSaveInstanceState + lifecycleScope.launchWhenResumed { + navigator.open4SSetup(this@HomeActivity, SetupMode.NORMAL) + } + } + private fun renderState(state: HomeActivityViewState) { when (val status = state.syncStatusServiceStatus) { is SyncStatusService.Status.InitialSyncProgressing -> { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index d460efb564..a16d710f4e 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -24,7 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() - data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction() + data class OpenGroup(val shouldClearFragment: Boolean) : HomeActivitySharedAction() + object CloseGroup : HomeActivitySharedAction() object AddSpace : HomeActivitySharedAction() data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index 5efd49a579..cb31a568e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -27,4 +27,6 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { object ShowAnalyticsOptIn : HomeActivityViewEvents object NotifyUserForThreadsMigration : HomeActivityViewEvents data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents + object StartRecoverySetupFlow : HomeActivityViewEvents + data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index c47ca0880e..9fe8a1f60e 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -17,7 +17,9 @@ package im.vector.app.features.home import androidx.lifecycle.asFlow +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -28,6 +30,9 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.store.AnalyticsStore import im.vector.app.features.login.ReAuthHelper +import im.vector.app.features.raw.wellknown.ElementWellKnown +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers @@ -42,6 +47,8 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.getUser @@ -59,8 +66,9 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException class HomeActivityViewModel @AssistedInject constructor( - @Assisted initialState: HomeActivityViewState, + @Assisted private val initialState: HomeActivityViewState, private val activeSessionHolder: ActiveSessionHolder, + private val rawService: RawService, private val reAuthHelper: ReAuthHelper, private val analyticsStore: AnalyticsStore, private val lightweightSettingsStorage: LightweightSettingsStorage, @@ -72,10 +80,17 @@ class HomeActivityViewModel @AssistedInject constructor( override fun create(initialState: HomeActivityViewState): HomeActivityViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? { + val activity: HomeActivity = viewModelContext.activity() + val args: HomeActivityArgs? = activity.intent.getParcelableExtra(Mavericks.KEY_ARG) + return args?.let { HomeActivityViewState(accountCreation = it.accountCreation) } + ?: super.initialState(viewModelContext) + } + } private var isInitialized = false - private var checkBootstrap = false + private var hasCheckedBootstrap = false private var onceTrusted = false private fun initialize() { @@ -116,17 +131,13 @@ class HomeActivityViewModel @AssistedInject constructor( safeActiveSession .flow() .liveCrossSigningInfo(safeActiveSession.myUserId) - .onEach { - val isVerified = it.getOrNull()?.isTrusted() ?: false + .onEach { info -> + val isVerified = info.getOrNull()?.isTrusted() ?: false if (!isVerified && onceTrusted) { - // cross signing keys have been reset - // Trigger a popup to re-verify - // Note: user can be null in case of logout - safeActiveSession.getUser(safeActiveSession.myUserId) - ?.toMatrixItem() - ?.let { user -> - _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) - } + viewModelScope.launch(Dispatchers.IO) { + val elementWellKnown = rawService.getElementWellknown(safeActiveSession.sessionParams) + sessionHasBeenUnverified(elementWellKnown) + } } onceTrusted = isVerified } @@ -180,15 +191,8 @@ class HomeActivityViewModel @AssistedInject constructor( .asFlow() .onEach { status -> when (status) { - is SyncStatusService.Status.InitialSyncProgressing -> { - // Schedule a check of the bootstrap when the init sync will be finished - checkBootstrap = true - } is SyncStatusService.Status.Idle -> { - if (checkBootstrap) { - checkBootstrap = false - maybeBootstrapCrossSigningAfterInitialSync() - } + maybeVerifyOrBootstrapCrossSigning() } else -> Unit } @@ -200,6 +204,10 @@ class HomeActivityViewModel @AssistedInject constructor( } } .launchIn(viewModelScope) + + if (session.hasAlreadySynced()) { + maybeVerifyOrBootstrapCrossSigning() + } } /** @@ -240,12 +248,72 @@ class HomeActivityViewModel @AssistedInject constructor( } } - private fun maybeBootstrapCrossSigningAfterInitialSync() { + private fun sessionHasBeenUnverified(elementWellKnown: ElementWellKnown?) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false + if (isSecureBackupRequired) { + // If 4S is forced, force verification + // for stability cancel all pending verifications? + session.cryptoService().verificationService().getExistingVerificationRequests(session.myUserId).forEach { + session.cryptoService().verificationService().cancelVerificationRequest(it) + } + _viewEvents.post(HomeActivityViewEvents.ForceVerification(false)) + } else { + // cross signing keys have been reset + // Trigger a popup to re-verify + // Note: user can be null in case of logout + session.getUser(session.myUserId) + ?.toMatrixItem() + ?.let { user -> + _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) + } + } + } + + private fun maybeVerifyOrBootstrapCrossSigning() { + // The contents of this method should only run once + if (hasCheckedBootstrap) return + hasCheckedBootstrap = true + // We do not use the viewModel context because we do not want to tie this action to activity view model activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) { - val session = activeSessionHolder.getSafeActiveSession() ?: return@launch + val session = activeSessionHolder.getSafeActiveSession() ?: return@launch Unit.also { + Timber.w("## No session to init cross signing or bootstrap") + } - tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { + val elementWellKnown = rawService.getElementWellknown(session.sessionParams) + val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false + + // In case of account creation, it is already done before + if (initialState.accountCreation) { + if (isSecureBackupRequired) { + _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) + } else { + val password = reAuthHelper.data ?: return@launch Unit.also { + Timber.w("No password to init cross signing") + } + + // Silently initialize cross signing without 4S + // We do not use the viewModel context because we do not want to cancel this action + Timber.d("Initialize cross signing") + try { + session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, _ -> + resume( + UserPasswordAuth( + session = response.session, + user = session.myUserId, + password = password + ) + ) + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") + } + } + return@launch + } + + tryOrNull("## MaybeVerifyOrBootstrapCrossSigning: Failed to download keys") { awaitCallback> { session.cryptoService().downloadKeys(listOf(session.myUserId), true, it) } @@ -255,47 +323,68 @@ class HomeActivityViewModel @AssistedInject constructor( // Is there already cross signing keys here? val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() if (mxCrossSigningInfo != null) { - // Cross-signing is already set up for this user, is it trusted? - if (!mxCrossSigningInfo.isTrusted()) { - // New session - _viewEvents.post( - HomeActivityViewEvents.OnNewSession( - session.getUser(session.myUserId)?.toMatrixItem(), - // Always send request instead of waiting for an incoming as per recent EW changes - false + if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup()) { + // If 4S is forced, start the full interactive setup flow + _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) + } else { + // Cross-signing is already set up for this user, is it trusted? + if (!mxCrossSigningInfo.isTrusted()) { + if (isSecureBackupRequired) { + // If 4S is forced, force verification + _viewEvents.post(HomeActivityViewEvents.ForceVerification(true)) + } else { + // New session + _viewEvents.post( + HomeActivityViewEvents.OnNewSession( + session.getUser(session.myUserId)?.toMatrixItem(), + // Always send request instead of waiting for an incoming as per recent EW changes + false + ) ) - ) + } + } } } else { - // Try to initialize cross signing in background if possible - Timber.d("Initialize cross signing...") - try { - awaitCallback { - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - // We missed server grace period or it's not setup, see if we remember locally password - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && - errCode == null && - reAuthHelper.data != null) { - promise.resume( - UserPasswordAuth( - session = flowResponse.session, - user = session.myUserId, - password = reAuthHelper.data - ) + // Cross signing is not initialized + if (isSecureBackupRequired) { + // If 4S is forced, start the full interactive setup flow + _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) + } else { + // Initialize cross-signing silently + val password = reAuthHelper.data + + if (password == null) { + // Check this is not an SSO account + if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) { + // Ask password to the user: Upgrade security + _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem())) + } + // Else (SSO) just ignore for the moment + } else { + // Try to initialize cross signing in background if possible + Timber.d("Initialize cross signing...") + try { + session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, errCode -> + // We missed server grace period or it's not setup, see if we remember locally password + if (response.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + errCode == null && + reAuthHelper.data != null) { + resume( + UserPasswordAuth( + session = response.session, + user = session.myUserId, + password = reAuthHelper.data ) - } else { - promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) - } - } - }, - callback = it - ) - Timber.d("Initialize cross signing SUCCESS") + ) + Timber.d("Initialize cross signing SUCCESS") + } else { + resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) + } + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") + } } - } catch (failure: Throwable) { - Timber.e(failure, "Failed to initialize cross signing") } } } @@ -312,3 +401,18 @@ class HomeActivityViewModel @AssistedInject constructor( } } } + +private suspend fun CrossSigningService.awaitCrossSigninInitialization( + block: Continuation.(response: RegistrationFlowResponse, errCode: String?) -> Unit +) { + awaitCallback { + initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.block(flowResponse, errCode) + } + }, + callback = it + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index 68131e8569..45fe04fc61 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -20,5 +20,6 @@ import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.initsync.SyncStatusService data class HomeActivityViewState( - val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle + val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle, + val accountCreation: Boolean = false ) : MavericksState 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 2753ba817d..4eedb528d1 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 @@ -33,6 +33,7 @@ import im.vector.app.R import im.vector.app.RoomGroupingMethod import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -69,7 +70,8 @@ class HomeDetailFragment @Inject constructor( private val appStateHandler: AppStateHandler ) : VectorBaseFragment(), KeysBackupBanner.Delegate, - CurrentCallsView.Callback { + CurrentCallsView.Callback, + OnBackPressed { private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() @@ -130,12 +132,8 @@ class HomeDetailFragment @Inject constructor( viewModel.onEach(HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod -> when (roomGroupingMethod) { - is RoomGroupingMethod.ByLegacyGroup -> { - onGroupChange(roomGroupingMethod.groupSummary) - } - is RoomGroupingMethod.BySpace -> { - onSpaceChange(roomGroupingMethod.spaceSummary) - } + is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary) + is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary) } } @@ -157,7 +155,6 @@ class HomeDetailFragment @Inject constructor( unknownDeviceDetectorSharedViewModel.onEach { state -> state.unknownSessions.invoke()?.let { unknownDevices -> -// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}") if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { val uid = "review_login" alertManager.cancelAlert(uid) @@ -190,6 +187,27 @@ class HomeDetailFragment @Inject constructor( } } + private fun navigateBack() { + try { + val lastSpace = appStateHandler.getSpaceBackstack().removeLast() + setCurrentSpace(lastSpace) + } catch (e: NoSuchElementException) { + navigateUpOneSpace() + } + } + + private fun setCurrentSpace(spaceId: String?) { + appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false) + sharedActionViewModel.post(HomeActivitySharedAction.CloseGroup) + } + + private fun navigateUpOneSpace() { + val parentId = getCurrentSpace()?.flattenParentIds?.lastOrNull() + setCurrentSpace(parentId) + } + + private fun getCurrentSpace() = (appStateHandler.getCurrentRoomGroupingMethod() as? RoomGroupingMethod.BySpace)?.spaceSummary + private fun handleCallStarted() { dismissLoadingDialog() val fragmentTag = HomeTab.DialPad.toFragmentTag() @@ -203,20 +221,16 @@ class HomeDetailFragment @Inject constructor( override fun onResume() { super.onResume() - // update notification tab if needed updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab()) callManager.checkForProtocolsSupportIfNeeded() + refreshSpaceState() + } - // Current space/group is not live so at least refresh toolbar on resume - appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod -> - when (roomGroupingMethod) { - is RoomGroupingMethod.ByLegacyGroup -> { - onGroupChange(roomGroupingMethod.groupSummary) - } - is RoomGroupingMethod.BySpace -> { - onSpaceChange(roomGroupingMethod.spaceSummary) - } - } + private fun refreshSpaceState() { + when (val roomGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) { + is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary) + is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary) + else -> Unit } } @@ -260,12 +274,12 @@ class HomeDetailFragment @Inject constructor( viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary) contentAction = Runnable { - (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { + (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity -> // mark as ignored to avoid showing it again unknownDeviceDetectorSharedViewModel.handle( UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId }) ) - it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS) + activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS) } } dismissedAction = Runnable { @@ -324,11 +338,11 @@ class HomeDetailFragment @Inject constructor( withState(viewModel) { when (it.roomGroupingMethod) { is RoomGroupingMethod.ByLegacyGroup -> { - // nothing do far + // do nothing } is RoomGroupingMethod.BySpace -> { - it.roomGroupingMethod.spaceSummary?.let { - sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(it.roomId)) + it.roomGroupingMethod.spaceSummary?.let { spaceSummary -> + sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId)) } } } @@ -348,17 +362,6 @@ class HomeDetailFragment @Inject constructor( viewModel.handle(HomeDetailAction.SwitchTab(tab)) true } - -// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView - -// bottomNavigationView.getOrCreateBadge() -// menuView.forEachIndexed { index, view -> -// val itemView = view as BottomNavigationItemView -// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false) -// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView) -// itemView.addView(badgeLayout) -// unreadCounterBadgeViews.add(index, unreadCounterBadgeView) -// } } private fun updateUIForTab(tab: HomeTab) { @@ -436,7 +439,6 @@ class HomeDetailFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { -// Timber.v(it.toString()) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) @@ -496,4 +498,11 @@ class HomeDetailFragment @Inject constructor( } return this } + + override fun onBackPressed(toolbarButton: Boolean) = if (getCurrentSpace() != null) { + navigateBack() + true + } else { + false + } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index a4f1566eff..30bff45cbd 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -45,8 +45,9 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.initsync.SyncStatusService @@ -241,7 +242,7 @@ class HomeDetailViewModel @AssistedInject constructor( roomSummaryQueryParams { memberships = listOf(Membership.INVITE) roomCategoryFilter = RoomCategoryFilter.ONLY_DM - activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None + spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) } } ).size @@ -249,7 +250,7 @@ class HomeDetailViewModel @AssistedInject constructor( roomSummaryQueryParams { memberships = listOf(Membership.INVITE) roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId) + spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms() } ).size } @@ -258,7 +259,7 @@ class HomeDetailViewModel @AssistedInject constructor( roomSummaryQueryParams { memberships = listOf(Membership.JOIN) roomCategoryFilter = RoomCategoryFilter.ONLY_DM - activeSpaceFilter = activeSpaceRoomId?.let { ActiveSpaceFilter.ActiveSpace(it) } ?: ActiveSpaceFilter.None + spaceFilter = activeSpaceRoomId?.let { SpaceFilter.ActiveSpace(it) } } ) @@ -266,7 +267,7 @@ class HomeDetailViewModel @AssistedInject constructor( roomSummaryQueryParams { memberships = listOf(Membership.JOIN) roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(groupingMethod.spaceSummary?.roomId) + spaceFilter = groupingMethod.toActiveSpaceOrOrphanRooms() } ) @@ -287,4 +288,8 @@ class HomeDetailViewModel @AssistedInject constructor( } .launchIn(viewModelScope) } + + private fun RoomGroupingMethod.BySpace.toActiveSpaceOrOrphanRooms(): SpaceFilter? { + return spaceSummary?.roomId?.toActiveSpaceOrOrphanRooms() + } } diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt index c1b3937fee..09e2d11f84 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import org.matrix.android.sdk.api.query.ActiveSpaceFilter +import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership @@ -74,11 +74,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia private val roomService = session.roomService() init { - roomService.getPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) - this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) + this.spaceFilter = SpaceFilter.OrphanRooms }, sortOrder = RoomSortOrder.NONE ).asFlow() .throttleFirst(300) @@ -86,7 +85,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia val counts = roomService.getNotificationCountForRooms( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) - this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) + this.spaceFilter = SpaceFilter.OrphanRooms } ) val invites = if (autoAcceptInvites.hideInvites) { @@ -95,7 +94,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia roomService.getRoomSummaries( roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) - this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) + this.spaceFilter = SpaceFilter.OrphanRooms } ).size } @@ -151,9 +150,9 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia val totalCount = roomService.getNotificationCountForRooms( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) - this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { + this.spaceFilter = SpaceFilter.OrphanRooms.takeIf { !vectorPreferences.prefSpacesShowAllRoomInHome() - } ?: ActiveSpaceFilter.None + } } ) 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 635b00c05d..3120decd0c 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 @@ -74,6 +74,9 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.containsRtLOverride +import im.vector.app.core.extensions.ensureEndsLeftToRight +import im.vector.app.core.extensions.filterDirectionOverrides import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextOrHide @@ -221,6 +224,7 @@ 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.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent @@ -647,6 +651,13 @@ class TimelineFragment @Inject constructor( ) } + private fun navigateToLocationLiveMap() { + navigator.openLocationLiveMap( + context = requireContext(), + roomId = timelineArgs.roomId + ) + } + private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) { views.locationLiveStatusIndicator.isVisible = event.isVisible } @@ -706,31 +717,31 @@ class TimelineFragment @Inject constructor( } private fun createEmojiPopup(): EmojiPopup { - return EmojiPopup - .Builder - .fromRootView(views.rootConstraintLayout) - .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) - .setOnEmojiPopupShownListener { + return EmojiPopup( + rootView = views.rootConstraintLayout, + keyboardAnimationStyle = R.style.emoji_fade_animation_style, + onEmojiPopupShownListener = { views.composerLayout.views.composerEmojiButton.apply { contentDescription = getString(R.string.a11y_close_emoji_picker) setImageResource(R.drawable.ic_keyboard) } - } - .setOnEmojiPopupDismissListenerLifecycleAware { + }, + onEmojiPopupDismissListener = lifecycleAwareDismissAction { views.composerLayout.views.composerEmojiButton.apply { contentDescription = getString(R.string.a11y_open_emoji_picker) setImageResource(R.drawable.ic_insert_emoji) } - } - .build(views.composerLayout.views.composerEditText) + }, + editText = views.composerLayout.views.composerEditText + ) } /** * Ensure dismiss actions only trigger when the fragment is in the started state. * EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView. */ - private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder { - return setOnEmojiPopupDismissListener { + private fun lifecycleAwareDismissAction(action: () -> Unit): () -> Unit { + return { if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { action() } @@ -1914,23 +1925,20 @@ class TimelineFragment @Inject constructor( } }) if (!isManaged) { - if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { - MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive) - .setTitle(R.string.external_link_confirmation_title) - .setMessage( - getString(R.string.external_link_confirmation_message, title, url) - .toSpannable() - .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - ) - .setPositiveButton(R.string._continue) { _, _ -> - openUrlInExternalBrowser(requireContext(), url) - } - .setNegativeButton(R.string.action_cancel, null) - .show() - } else { - // Open in external browser, in a new Tab - openUrlInExternalBrowser(requireContext(), url) + when { + url.containsRtLOverride() -> { + displayUrlConfirmationDialog( + seenUrl = title.ensureEndsLeftToRight(), + actualUrl = url.filterDirectionOverrides(), + continueTo = url + ) + } + title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host -> { + displayUrlConfirmationDialog(title, url) + } + else -> { + openUrlInExternalBrowser(requireContext(), url) + } } } } @@ -1938,6 +1946,22 @@ class TimelineFragment @Inject constructor( return true } + private fun displayUrlConfirmationDialog(seenUrl: String, actualUrl: String, continueTo: String = actualUrl) { + MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive) + .setTitle(R.string.external_link_confirmation_title) + .setMessage( + getString(R.string.external_link_confirmation_message, seenUrl, actualUrl) + .toSpannable() + .colorizeMatchingText(actualUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + .colorizeMatchingText(seenUrl, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + ) + .setPositiveButton(R.string._continue) { _, _ -> + openUrlInExternalBrowser(requireContext(), continueTo) + } + .setNegativeButton(R.string.action_cancel, null) + .show() + } + override fun onUrlLongClicked(url: String): Boolean { if (url != getString(R.string.edited_suffix) && url.isValidUrl()) { // Copy the url to the clipboard @@ -2015,6 +2039,9 @@ class TimelineFragment @Inject constructor( is MessageLocationContent -> { handleShowLocationPreview(messageContent, informationData.senderId) } + is MessageBeaconInfoContent -> { + navigateToLocationLiveMap() + } else -> { val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) if (!handled) { 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 e81bab3e3e..86137b89f1 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 @@ -211,7 +211,8 @@ class TimelineViewModel @AssistedInject constructor( appStateHandler.getCurrentRoomGroupingMethod()?.space().let { currentSpace -> val currentRoomSummary = room.roomSummary() ?: return@let // nothing we are good - if (currentSpace == null || !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId)) { + if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) || + (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) { // take first one or switch to home appStateHandler.setCurrentSpace( currentRoomSummary diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 5f12c2f174..e3b10ef8c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -52,7 +52,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) object ViewInRoom : - EventSharedAction(R.string.view_in_room, R.drawable.ic_thread_view_in_room_menu_item) + EventSharedAction(R.string.view_in_room, R.drawable.ic_threads_view_in_room_24) data class Share(val eventId: String, val messageContent: MessageContent) : EventSharedAction(R.string.action_share, R.drawable.ic_share) 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 4bc513284b..79a6dd27ff 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 @@ -401,8 +401,8 @@ class MessageActionsViewModel @AssistedInject constructor( if (vectorPreferences.developerMode()) { if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) { val keysBackupService = session.cryptoService().keysBackupService() - if (keysBackupService.state == KeysBackupState.NotTrusted || - (keysBackupService.state == KeysBackupState.ReadyToBackUp && + if (keysBackupService.getState() == KeysBackupState.NotTrusted || + (keysBackupService.getState() == KeysBackupState.ReadyToBackUp && keysBackupService.canRestoreKeys()) ) { add(EventSharedAction.UseKeyBackup) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt deleted file mode 100644 index d233deffb8..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.detail.timeline.factory - -import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider -import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem -import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_ -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent -import javax.inject.Inject - -class LiveLocationMessageItemFactory @Inject constructor( - private val dimensionConverter: DimensionConverter, - private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val avatarSizeProvider: AvatarSizeProvider, -) { - - fun create( - beaconInfoContent: MessageBeaconInfoContent, - highlight: Boolean, - attributes: AbsMessageItem.Attributes, - ): VectorEpoxyModel<*>? { - // TODO handle location received and stopped states - return when { - isLiveRunning(beaconInfoContent) -> buildStartLiveItem(highlight, attributes) - else -> null - } - } - - private fun isLiveRunning(beaconInfoContent: MessageBeaconInfoContent): Boolean { - // TODO when we will use aggregatedSummary, check if the live has timed out as well - return beaconInfoContent.isLive.orFalse() - } - - private fun buildStartLiveItem( - highlight: Boolean, - attributes: AbsMessageItem.Attributes, - ): MessageLiveLocationStartItem { - val width = timelineMediaSizeProvider.getMaxSize().first - val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) - - return MessageLiveLocationStartItem_() - .attributes(attributes) - .mapWidth(width) - .mapHeight(height) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt new file mode 100644 index 0000000000..3c7b6c32e1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt @@ -0,0 +1,167 @@ +/* + * 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.factory + +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.DateProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationInactiveItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationInactiveItem_ +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem_ +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_ +import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE +import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.toLocationData +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.threeten.bp.LocalDateTime +import timber.log.Timber +import javax.inject.Inject + +class LiveLocationShareMessageItemFactory @Inject constructor( + private val session: Session, + private val dimensionConverter: DimensionConverter, + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val avatarSizeProvider: AvatarSizeProvider, + private val urlMapProvider: UrlMapProvider, + private val locationPinProvider: LocationPinProvider, + private val vectorDateFormatter: VectorDateFormatter, +) { + + fun create( + event: TimelineEvent, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): VectorEpoxyModel<*>? { + val liveLocationShareSummaryData = getLiveLocationShareSummaryData(event) + val item = when (val currentState = getViewState(liveLocationShareSummaryData)) { + LiveLocationShareViewState.Inactive -> buildInactiveItem(highlight, attributes) + LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes) + is LiveLocationShareViewState.Running -> buildRunningItem(highlight, attributes, currentState) + LiveLocationShareViewState.Unkwown -> null + } + item?.layout(attributes.informationData.messageLayout.layoutRes) + + return item + } + + private fun buildInactiveItem( + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageLiveLocationInactiveItem { + val width = timelineMediaSizeProvider.getMaxSize().first + val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) + + return MessageLiveLocationInactiveItem_() + // disable the click on this state item + .attributes(attributes.copy(itemClickListener = null)) + .mapWidth(width) + .mapHeight(height) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } + + private fun buildLoadingItem( + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageLiveLocationStartItem { + val width = timelineMediaSizeProvider.getMaxSize().first + val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) + + return MessageLiveLocationStartItem_() + // disable the click on this state item + .attributes(attributes.copy(itemClickListener = null)) + .mapWidth(width) + .mapHeight(height) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } + + private fun buildRunningItem( + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + runningState: LiveLocationShareViewState.Running, + ): MessageLiveLocationItem { + // TODO only render location if enabled in preferences: to be handled in a next PR + val width = timelineMediaSizeProvider.getMaxSize().first + val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) + + val locationUrl = runningState.lastGeoUri.toLocationData()?.let { + urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) + } + + return MessageLiveLocationItem_() + .attributes(attributes) + .locationUrl(locationUrl) + .mapWidth(width) + .mapHeight(height) + .locationUserId(attributes.informationData.senderId) + .locationPinProvider(locationPinProvider) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .currentUserId(session.myUserId) + .endOfLiveDateTime(runningState.endOfLiveDateTime) + .vectorDateFormatter(vectorDateFormatter) + } + + private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState { + return when { + liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown + liveLocationShareSummaryData.isActive.not() -> LiveLocationShareViewState.Inactive + liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading + else -> + LiveLocationShareViewState.Running( + liveLocationShareSummaryData.lastGeoUri.orEmpty(), + getEndOfLiveDateTime(liveLocationShareSummaryData) + ) + }.also { viewState -> Timber.d("computed viewState: $viewState") } + } + + private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? { + return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) } + } + + private fun getLiveLocationShareSummaryData(event: TimelineEvent): LiveLocationShareSummaryData? { + return event.annotations?.liveLocationShareAggregatedSummary?.let { summary -> + LiveLocationShareSummaryData( + isActive = summary.isActive, + endOfLiveTimestampMillis = summary.endOfLiveTimestampMillis, + lastGeoUri = summary.lastLocationDataContent?.getBestLocationInfo()?.geoUri + ) + } + } + + private data class LiveLocationShareSummaryData( + val isActive: Boolean?, + val endOfLiveTimestampMillis: Long?, + val lastGeoUri: String?, + ) + + private sealed class LiveLocationShareViewState { + object Loading : LiveLocationShareViewState() + data class Running(val lastGeoUri: String, val endOfLiveDateTime: LocalDateTime?) : LiveLocationShareViewState() + object Inactive : LiveLocationShareViewState() + object Unkwown : LiveLocationShareViewState() + } +} 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 aca2aab174..224c1cdbea 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 @@ -55,8 +55,15 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde private val mergeItemCollapseStates = HashMap() /** + * @param event the main timeline event * @param nextEvent is an older event than event * @param items all known items, sorted from newer event to oldest event + * @param partialState partial state data + * @param addDaySeparator true to add a day separator + * @param currentPosition the current position + * @param eventIdToHighlight if not null the event which has to be highlighted + * @param callback callback for user event + * @param requestModelBuild lambda to let the built Item request a model build when the collapse state is changed */ fun create(event: TimelineEvent, nextEvent: TimelineEvent?, 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 b960e2c6a9..13f783cded 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 @@ -148,7 +148,7 @@ class MessageItemFactory @Inject constructor( private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, - private val liveLocationMessageItemFactory: LiveLocationMessageItemFactory, + private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, ) { // TODO inject this properly? @@ -216,7 +216,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } - is MessageBeaconInfoContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes) + is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { @@ -237,14 +237,14 @@ class MessageItemFactory @Inject constructor( urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) } - val userId = if (locationContent.isSelfLocation()) informationData.senderId else null + val locationUserId = if (locationContent.isSelfLocation()) informationData.senderId else null return MessageLocationItem_() .attributes(attributes) .locationUrl(locationUrl) .mapWidth(width) .mapHeight(height) - .userId(userId) + .locationUserId(locationUserId) .locationPinProvider(locationPinProvider) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index f4bcc1ba65..07ae9d66c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -100,7 +100,7 @@ class TimelineItemFactory @Inject constructor( // Message itemsX EventType.STICKER, in EventType.POLL_START, - EventType.MESSAGE -> messageItemFactory.create(params) + EventType.MESSAGE -> messageItemFactory.create(params) EventType.REDACTION, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, @@ -113,14 +113,15 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_NEGOTIATE, EventType.REACTION, in EventType.POLL_RESPONSE, - in EventType.POLL_END -> noticeItemFactory.create(params) + in EventType.POLL_END, + in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params) // Calls EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_REJECT, - EventType.CALL_ANSWER -> callItemFactory.create(params) + EventType.CALL_ANSWER -> callItemFactory.create(params) // Crypto - EventType.ENCRYPTED -> { + EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it messageItemFactory.create(params) @@ -129,11 +130,11 @@ class TimelineItemFactory @Inject constructor( } } EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE -> { + EventType.KEY_VERIFICATION_DONE -> { verificationConclusionItemFactory.create(params) } // Unhandled event types - else -> { + else -> { // Should only happen when shouldShowHiddenEvents() settings is ON Timber.v("Type ${event.root.getClearType()} not handled") defaultItemFactory.create(params) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 7ad0cb27c6..8e06b3ee5d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -107,7 +107,8 @@ class NoticeEventFormatter @Inject constructor( EventType.REDACTION, EventType.STICKER, in EventType.POLL_RESPONSE, - in EventType.POLL_END -> formatDebug(timelineEvent.root) + in EventType.POLL_END, + in EventType.BEACON_LOCATION_DATA -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 1e95f067d2..7e4ec75783 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent @@ -44,8 +45,7 @@ import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited import javax.inject.Inject /** - * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline. - * TODO Update this comment + * This class is responsible of building extra information data associated to a given event. */ class MessageInformationDataFactory @Inject constructor(private val session: Session, private val dateFormatter: VectorDateFormatter, @@ -119,7 +119,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses isFirstFromThisSender = isFirstFromThisSender, isLastFromThisSender = isLastFromThisSender, e2eDecoration = e2eDecoration, - sendStateDecoration = sendStateDecoration + sendStateDecoration = sendStateDecoration, + messageType = event.root.getMsgType() ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 45c711ff93..dad0e3e4ed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -57,6 +57,7 @@ class MessageItemAttributesFactory @Inject constructor( memberClickListener = { callback?.onMemberNameClicked(informationData) }, + callback = callback, reactionPillCallback = callback, avatarCallback = callback, threadCallback = callback, @@ -66,7 +67,8 @@ class MessageItemAttributesFactory @Inject constructor( threadSummaryFormatted = displayableEventFormatter.formatThreadSummary(threadDetails?.threadSummaryLatestEvent).toString(), threadDetails = threadDetails, reactionsSummaryEvents = reactionsSummaryEvents, - areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled() + areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled(), + autoplayAnimatedImages = preferencesProvider.autoplayAnimatedImages() ) } } 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 f317eb4f9a..8ca999309a 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 @@ -36,6 +36,8 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility + * @param rootThreadEventId the root thread event id if in a thread timeline + * @param isFromThreadTimeline true if the timeline is a thread * * @return a list of timeline events which have sequentially the same type following the next direction. */ @@ -86,6 +88,8 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility + * @param rootThreadEventId the root thread eventId + * @param isFromThreadTimeline true if the timeline is a thread * * @return a list of timeline events which have sequentially the same type following the prev direction. */ @@ -107,6 +111,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen /** * @param timelineEvent the event to check for visibility * @param highlightedEventId can be checked to force visibility to true + * @param isFromThreadTimeline true if the timeline is a thread * @param rootThreadEventId if this param is null it means we are in the original timeline * @return true if the event should be shown in the timeline. */ 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 263f105b6b..24a148885f 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 @@ -178,6 +178,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val itemLongClickListener: View.OnLongClickListener? = null, override val itemClickListener: ClickListener? = null, val memberClickListener: ClickListener? = null, + val callback: TimelineEventController.Callback? = null, override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null, val threadCallback: TimelineEventController.ThreadCallback? = null, @@ -187,6 +188,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem val threadSummaryFormatted: String? = null, val threadDetails: ThreadDetails? = null, val areThreadMessagesEnabled: Boolean = false, + val autoplayAnimatedImages: Boolean = false, override val reactionsSummaryEvents: ReactionsSummaryEvents? = null, ) : AbsBaseMessageItem.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 new file mode 100644 index 0000000000..e7823845fa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021 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.item + +import android.graphics.drawable.Drawable +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import im.vector.app.R +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +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() { + + @EpoxyAttribute + var locationUrl: String? = null + + @EpoxyAttribute + var locationUserId: String? = null + + @EpoxyAttribute + var mapWidth: Int = 0 + + @EpoxyAttribute + var mapHeight: Int = 0 + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var locationPinProvider: LocationPinProvider? = null + + override fun bind(holder: H) { + super.bind(holder) + renderSendState(holder.view, null) + bindMap(holder) + } + + private fun bindMap(holder: Holder) { + val location = locationUrl ?: return + val messageLayout = attributes.informationData.messageLayout + val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + messageLayout.cornersRadius.granularRoundedCorners() + } else { + val dimensionConverter = DimensionConverter(holder.view.resources) + RoundedCorners(dimensionConverter.dpToPx(8)) + } + holder.staticMapImageView.updateLayoutParams { + width = mapWidth + height = mapHeight + } + GlideApp.with(holder.staticMapImageView) + .load(location) + .apply(RequestOptions.centerCropTransform()) + .placeholder(holder.staticMapImageView.drawable) + .listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed) + holder.staticMapErrorTextView.isVisible = true + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + locationPinProvider?.create(locationUserId) { pinDrawable -> + // we are not using Glide since it does not display it correctly when there is no user photo + holder.staticMapPinImageView.setImageDrawable(pinDrawable) + } + holder.staticMapErrorTextView.isVisible = false + return false + } + }) + .transform(imageCornerTransformation) + .into(holder.staticMapImageView) + } + + abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { + val staticMapImageView by bind(R.id.staticMapImageView) + val staticMapPinImageView by bind(R.id.staticMapPinImageView) + val staticMapErrorTextView by bind(R.id.staticMapErrorTextView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt new file mode 100644 index 0000000000..c421efda12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.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.home.room.detail.timeline.item + +import android.content.res.Resources +import android.graphics.drawable.ColorDrawable +import android.widget.ImageView +import androidx.core.view.updateLayoutParams +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import im.vector.app.R +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners +import im.vector.app.features.themes.ThemeUtils + +/** + * Default implementation of common methods for item representing the status of a live location share. + */ +class DefaultLiveLocationShareStatusItem : LiveLocationShareStatusItem { + + override fun bindMap( + mapImageView: ImageView, + mapWidth: Int, + mapHeight: Int, + messageLayout: TimelineMessageLayout + ) { + val mapCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + messageLayout.cornersRadius.granularRoundedCorners() + } else { + RoundedCorners(getDefaultLayoutCornerRadiusInDp(mapImageView.resources)) + } + mapImageView.updateLayoutParams { + width = mapWidth + height = mapHeight + } + GlideApp.with(mapImageView) + .load(R.drawable.bg_no_location_map) + .transform(mapCornerTransformation) + .into(mapImageView) + } + + override fun bindBottomBanner(bannerImageView: ImageView, messageLayout: TimelineMessageLayout) { + val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + GranularRoundedCorners( + 0f, + 0f, + messageLayout.cornersRadius.bottomEndRadius, + messageLayout.cornersRadius.bottomStartRadius + ) + } else { + val bottomCornerRadius = getDefaultLayoutCornerRadiusInDp(bannerImageView.resources).toFloat() + GranularRoundedCorners(0f, 0f, bottomCornerRadius, bottomCornerRadius) + } + GlideApp.with(bannerImageView) + .load(ColorDrawable(ThemeUtils.getColor(bannerImageView.context, android.R.attr.colorBackground))) + .transform(imageCornerTransformation) + .into(bannerImageView) + } + + private fun getDefaultLayoutCornerRadiusInDp(resources: Resources): Int { + val dimensionConverter = DimensionConverter(resources) + return dimensionConverter.dpToPx(8) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt new file mode 100644 index 0000000000..2f79f2fc9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.widget.ImageView +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout + +interface LiveLocationShareStatusItem { + fun bindMap( + mapImageView: ImageView, + mapWidth: Int, + mapHeight: Int, + messageLayout: TimelineMessageLayout + ) + + fun bindBottomBanner(bannerImageView: ImageView, messageLayout: TimelineMessageLayout) +} 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 f3ca525136..f41c17d9e7 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 @@ -135,7 +135,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem() { @@ -80,7 +81,17 @@ abstract class MessageImageVideoItem : AbsMessageItem(), + LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() { + + @EpoxyAttribute + var mapWidth: Int = 0 + + @EpoxyAttribute + var mapHeight: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.view, null) + bindMap(holder.noLocationMapImageView, mapWidth, mapHeight, attributes.informationData.messageLayout) + bindBottomBanner(holder.bannerImageView, attributes.informationData.messageLayout) + } + + override fun getViewStubId() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val bannerImageView by bind(R.id.locationLiveInactiveBanner) + val noLocationMapImageView by bind(R.id.locationLiveInactiveMap) + } + + companion object { + private const val STUB_ID = R.id.messageContentLiveLocationInactiveStub + } +} 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 new file mode 100644 index 0000000000..838fbd46de --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +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.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.toTimestamp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.RoomDetailAction +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +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) +abstract class MessageLiveLocationItem : AbsMessageLocationItem() { + + @EpoxyAttribute + var currentUserId: String? = null + + @EpoxyAttribute + var endOfLiveDateTime: LocalDateTime? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var vectorDateFormatter: VectorDateFormatter + + override fun bind(holder: Holder) { + super.bind(holder) + bindLocationLiveBanner(holder) + } + + private fun bindLocationLiveBanner(holder: Holder) { + // TODO in a future PR add check on device id to confirm that is the one that sent the beacon + val isEmitter = currentUserId != null && currentUserId == locationUserId + val messageLayout = attributes.informationData.messageLayout + val viewState = buildViewState(holder, messageLayout, isEmitter) + holder.locationLiveMessageBanner.isVisible = true + holder.locationLiveMessageBanner.render(viewState) + holder.locationLiveMessageBanner.stopButton.setOnClickListener { + attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing) + } + } + + private fun buildViewState( + holder: Holder, + messageLayout: TimelineMessageLayout, + isEmitter: Boolean + ): LocationLiveMessageBannerViewState { + return when { + messageLayout is TimelineMessageLayout.Bubble && isEmitter -> + LocationLiveMessageBannerViewState.Emitter( + remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), + bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius, + bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius, + isStopButtonCenteredVertically = false + ) + messageLayout is TimelineMessageLayout.Bubble -> + LocationLiveMessageBannerViewState.Watcher( + bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius, + bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius, + formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(), + ) + isEmitter -> { + val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder) + LocationLiveMessageBannerViewState.Emitter( + remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), + bottomStartCornerRadiusInDp = cornerRadius, + bottomEndCornerRadiusInDp = cornerRadius, + isStopButtonCenteredVertically = true + ) + } + else -> { + val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder) + LocationLiveMessageBannerViewState.Watcher( + bottomStartCornerRadiusInDp = cornerRadius, + bottomEndCornerRadiusInDp = cornerRadius, + formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(), + ) + } + } + } + + private fun getBannerCornerRadiusForDefaultLayout(holder: Holder): Float { + val dimensionConverter = DimensionConverter(holder.view.resources) + return dimensionConverter.dpToPx(8).toFloat() + } + + private fun getFormattedLocalTimeEndOfLive() = + endOfLiveDateTime?.toTimestamp()?.let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) }.orEmpty() + + private fun getRemainingTimeOfLiveInMillis() = + (endOfLiveDateTime?.toTimestamp() ?: 0) - LocalDateTime.now().toTimestamp() + + override fun getViewStubId() = STUB_ID + + class Holder : AbsMessageLocationItem.Holder(STUB_ID) { + val locationLiveMessageBanner by bind(R.id.locationLiveMessageBanner) + } + + companion object { + private const val STUB_ID = R.id.messageContentLiveLocationStub + } +} 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 390db0ef50..001774b579 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 @@ -16,22 +16,15 @@ package im.vector.app.features.home.room.detail.timeline.item -import android.graphics.drawable.ColorDrawable import android.widget.ImageView -import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners -import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R -import im.vector.app.core.glide.GlideApp -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout -import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners -import im.vector.app.features.themes.ThemeUtils @EpoxyModelClass(layout = R.layout.item_timeline_event_base) -abstract class MessageLiveLocationStartItem : AbsMessageItem() { +abstract class MessageLiveLocationStartItem : + AbsMessageItem(), + LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() { @EpoxyAttribute var mapWidth: Int = 0 @@ -42,44 +35,8 @@ abstract class MessageLiveLocationStartItem : AbsMessageItem() { - - @EpoxyAttribute - var locationUrl: String? = null - - @EpoxyAttribute - var userId: String? = null - - @EpoxyAttribute - var mapWidth: Int = 0 - - @EpoxyAttribute - var mapHeight: Int = 0 - - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) - var locationPinProvider: LocationPinProvider? = null - - override fun bind(holder: Holder) { - super.bind(holder) - renderSendState(holder.view, null) - val location = locationUrl ?: return - val messageLayout = attributes.informationData.messageLayout - val dimensionConverter = DimensionConverter(holder.view.resources) - val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { - messageLayout.cornersRadius.granularRoundedCorners() - } else { - RoundedCorners(dimensionConverter.dpToPx(8)) - } - holder.staticMapImageView.updateLayoutParams { - width = mapWidth - height = mapHeight - } - GlideApp.with(holder.staticMapImageView) - .load(location) - .apply(RequestOptions.centerCropTransform()) - .listener(object : RequestListener { - override fun onLoadFailed(e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean): Boolean { - holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed) - holder.staticMapErrorTextView.isVisible = true - return false - } - - override fun onResourceReady(resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean): Boolean { - locationPinProvider?.create(userId) { pinDrawable -> - GlideApp.with(holder.staticMapPinImageView) - .load(pinDrawable) - .into(holder.staticMapPinImageView) - } - holder.staticMapErrorTextView.isVisible = false - return false - } - }) - .transform(imageCornerTransformation) - .into(holder.staticMapImageView) - } +abstract class MessageLocationItem : AbsMessageLocationItem() { override fun getViewStubId() = STUB_ID - class Holder : AbsMessageItem.Holder(STUB_ID) { - val staticMapImageView by bind(R.id.staticMapImageView) - val staticMapPinImageView by bind(R.id.staticMapPinImageView) - val staticMapErrorTextView by bind(R.id.staticMapErrorTextView) - } + class Holder : AbsMessageLocationItem.Holder(STUB_ID) companion object { private const val STUB_ID = R.id.messageContentLocationStub diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index cb0b2384ec..a0d10a8a75 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -66,6 +66,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_BEACON_INFO, ) + + private val MSG_TYPES_WITH_LOCATION_DATA = setOf( + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_BEACON_LOCATION_DATA + ) } private val cornerRadius: Float by lazy { @@ -145,9 +150,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess } private fun MessageContent?.timestampInsideMessage(): Boolean { - if (this == null) return false - if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() - return this.msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE + return when { + this == null -> false + msgType in MSG_TYPES_WITH_LOCATION_DATA -> vectorPreferences.labsRenderLocationsInTimeline() + else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE + } } private fun MessageContent?.shouldAddMessageOverlay(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index 3ae6a1fea5..69455c767e 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -59,6 +59,8 @@ class PreviewUrlView @JvmOverloads constructor( * This methods is responsible for rendering the view according to the newState. * * @param newState the newState representing the view + * @param imageContentRenderer the tool to render the image + * @param force true to force refresh */ fun render(newState: PreviewUrlUiState, imageContentRenderer: ImageContentRenderer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt index 8cf7e6bcab..35396d52d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt @@ -68,7 +68,7 @@ abstract class CollapsableTypedEpoxyController : } override fun buildModels() { - check(isBuildingModels()) { + check(isBuildingModels) { ("You cannot call `buildModels` directly. Call `setData` instead to trigger a model " + "refresh with new data.") } 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 2be6130a5d..674f5022f2 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 @@ -211,7 +211,7 @@ class RoomListFragment @Inject constructor( RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.isVisible = true RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true - else -> Unit // No button in this mode + RoomListDisplayMode.FILTERED -> Unit // No button in this mode } views.createChatRoomButton.debouncedClicks { @@ -237,7 +237,7 @@ class RoomListFragment @Inject constructor( RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.hide() RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.hide() RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.hide() - else -> Unit + RoomListDisplayMode.FILTERED -> Unit } } } @@ -294,7 +294,7 @@ class RoomListFragment @Inject constructor( val contentAdapter = when { section.livePages != null -> { - pagedControllerFactory.createRoomSummaryPagedController() + pagedControllerFactory.createRoomSummaryPagedController(roomListParams.displayMode) .also { controller -> section.livePages.observe(viewLifecycleOwner) { pl -> controller.submitList(pl) @@ -316,7 +316,7 @@ class RoomListFragment @Inject constructor( ) } } - section.isExpanded.observe(viewLifecycleOwner) { _ -> + section.isExpanded.observe(viewLifecycleOwner) { refreshCollapseStates() } controller.listener = this @@ -337,14 +337,14 @@ class RoomListFragment @Inject constructor( checkEmptyState() } observeItemCount(section, sectionAdapter) - section.isExpanded.observe(viewLifecycleOwner) { _ -> + section.isExpanded.observe(viewLifecycleOwner) { refreshCollapseStates() } controller.listener = this } } else -> { - pagedControllerFactory.createRoomSummaryListController() + pagedControllerFactory.createRoomSummaryListController(roomListParams.displayMode) .also { controller -> section.liveList?.observe(viewLifecycleOwner) { list -> controller.setData(list) @@ -366,7 +366,7 @@ class RoomListFragment @Inject constructor( ) } } - section.isExpanded.observe(viewLifecycleOwner) { _ -> + section.isExpanded.observe(viewLifecycleOwner) { refreshCollapseStates() } controller.listener = this @@ -402,7 +402,7 @@ class RoomListFragment @Inject constructor( RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.show() RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show() RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show() - else -> Unit + RoomListDisplayMode.FILTERED -> Unit } } } @@ -498,7 +498,7 @@ class RoomListFragment @Inject constructor( isBigImage = true, message = getString(R.string.room_list_rooms_empty_body) ) - else -> + RoomListDisplayMode.FILTERED -> // Always display the content in this mode, because if the footer StateView.State.Content } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index 80c7b4e921..87bbbd8757 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams class RoomListSectionBuilderGroup( private val coroutineScope: CoroutineScope, @@ -96,7 +97,6 @@ class RoomListSectionBuilderGroup( true ) { it.memberships = listOf(Membership.INVITE) - it.roomCategoryFilter = RoomCategoryFilter.ALL it.activeGroupId = actualGroupId } } @@ -281,9 +281,6 @@ class RoomListSectionBuilderGroup( } private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { - RoomSummaryQueryParams.Builder() - .apply { builder.invoke(this) } - .build() - .let { block(it) } + block(roomSummaryQueryParams { builder.invoke(this) }) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 59137ec490..e36d3da899 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -43,14 +43,16 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter +import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import timber.log.Timber @@ -297,7 +299,6 @@ class RoomListSectionBuilderSpace( countRoomAsNotif = true ) { it.memberships = listOf(Membership.INVITE) - it.roomCategoryFilter = RoomCategoryFilter.ALL } } @@ -323,9 +324,9 @@ class RoomListSectionBuilderSpace( { it.memberships = Membership.activeMemberships() }, - { qpm -> + { queryParams -> val name = stringProvider.getString(R.string.bottom_action_rooms) - val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(qpm) + val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams) onUpdatable(updatableFilterLivePageResult) val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() @@ -370,7 +371,7 @@ class RoomListSectionBuilderSpace( activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { override fun updateForSpaceId(roomId: String?) { filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + spaceFilter = roomId?.toActiveSpaceOrOrphanRooms() ) liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams } } @@ -381,11 +382,11 @@ class RoomListSectionBuilderSpace( override fun updateForSpaceId(roomId: String?) { if (roomId != null) { filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + spaceFilter = SpaceFilter.ActiveSpace(roomId) ) } else { filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.None + spaceFilter = null ) } liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams } @@ -429,29 +430,20 @@ class RoomListSectionBuilderSpace( } private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { - RoomSummaryQueryParams.Builder() - .apply { builder.invoke(this) } - .build() - .let { block(it) } + block(roomSummaryQueryParams { builder.invoke(this) }) } internal fun RoomSummaryQueryParams.process(spaceFilter: RoomListViewModel.SpaceFilterStrategy, currentSpace: String?): RoomSummaryQueryParams { return when (spaceFilter) { RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> { copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace) + spaceFilter = currentSpace?.toActiveSpaceOrOrphanRooms() ) } RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> { - if (currentSpace == null) { - copy( - activeSpaceFilter = ActiveSpaceFilter.None - ) - } else { - copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(currentSpace) - ) - } + copy( + spaceFilter = currentSpace?.let { SpaceFilter.ActiveSpace(it) } + ) } RoomListViewModel.SpaceFilterStrategy.NONE -> this } 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 70c5846646..5452b03992 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 @@ -18,9 +18,9 @@ package im.vector.app.features.home.room.list import android.view.HapticFeedbackConstants import android.view.View -import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -36,6 +36,7 @@ import im.vector.app.core.ui.views.PresenceStateImageView import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.themes.ThemeUtils import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel @@ -45,48 +46,102 @@ import org.matrix.android.sdk.api.util.MatrixItem @EpoxyModelClass(layout = R.layout.item_room) abstract class RoomSummaryItem : VectorEpoxyModel() { - @EpoxyAttribute lateinit var typingMessage: String - @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute + lateinit var typingMessage: String - @EpoxyAttribute lateinit var lastFormattedEvent: EpoxyCharSequence - @EpoxyAttribute lateinit var lastEventTime: String - @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null - @EpoxyAttribute var userPresence: UserPresence? = null - @EpoxyAttribute var showPresence: Boolean = false - @EpoxyAttribute var izPublic: Boolean = false - @EpoxyAttribute var unreadNotificationCount: Int = 0 - @EpoxyAttribute var hasUnreadMessage: Boolean = false - @EpoxyAttribute var hasDraft: Boolean = false - @EpoxyAttribute var showHighlighted: Boolean = false - @EpoxyAttribute var hasFailedSending: Boolean = false - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemLongClickListener: View.OnLongClickListener? = null - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null - @EpoxyAttribute var showSelected: Boolean = false + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + lateinit var matrixItem: MatrixItem + + @EpoxyAttribute + var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE + + @EpoxyAttribute + lateinit var subtitle: String + + @EpoxyAttribute + lateinit var lastFormattedEvent: EpoxyCharSequence + + @EpoxyAttribute + lateinit var lastEventTime: String + + @EpoxyAttribute + var encryptionTrustLevel: RoomEncryptionTrustLevel? = null + + @EpoxyAttribute + var userPresence: UserPresence? = null + + @EpoxyAttribute + var showPresence: Boolean = false + + @EpoxyAttribute @JvmField + var isPublic: Boolean = false + + @EpoxyAttribute + var unreadNotificationCount: Int = 0 + + @EpoxyAttribute + var hasUnreadMessage: Boolean = false + + @EpoxyAttribute + var hasDraft: Boolean = false + + @EpoxyAttribute + var showHighlighted: Boolean = false + + @EpoxyAttribute + var hasFailedSending: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemLongClickListener: View.OnLongClickListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemClickListener: ClickListener? = null + + @EpoxyAttribute + var showSelected: Boolean = false override fun bind(holder: Holder) { super.bind(holder) + + renderDisplayMode(holder) holder.rootView.onClick(itemClickListener) holder.rootView.setOnLongClickListener { it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) itemLongClickListener?.onLongClick(it) ?: false } holder.titleView.text = matrixItem.getBestName() - holder.lastEventTimeView.text = lastEventTime - holder.lastEventView.text = lastFormattedEvent.charSequence holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.unreadIndentIndicator.isVisible = hasUnreadMessage holder.draftView.isVisible = hasDraft avatarRenderer.render(matrixItem, holder.avatarImageView) holder.roomAvatarDecorationImageView.render(encryptionTrustLevel) - holder.roomAvatarPublicDecorationImageView.isVisible = izPublic + holder.roomAvatarPublicDecorationImageView.isVisible = isPublic holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending renderSelection(holder, showSelected) - holder.typingView.setTextOrHide(typingMessage) - holder.lastEventView.isInvisible = holder.typingView.isVisible holder.roomAvatarPresenceImageView.render(showPresence, userPresence) } + private fun renderDisplayMode(holder: Holder) = when (displayMode) { + RoomListDisplayMode.ROOMS, + RoomListDisplayMode.PEOPLE, + RoomListDisplayMode.NOTIFICATIONS -> renderForDefaultDisplayMode(holder) + RoomListDisplayMode.FILTERED -> renderForFilteredDisplayMode(holder) + } + + private fun renderForDefaultDisplayMode(holder: Holder) { + holder.subtitleView.text = lastFormattedEvent.charSequence + holder.lastEventTimeView.text = lastEventTime + holder.typingView.setTextOrHide(typingMessage) + holder.subtitleView.isInvisible = holder.typingView.isVisible + } + + private fun renderForFilteredDisplayMode(holder: Holder) { + holder.subtitleView.text = subtitle + } + override fun unbind(holder: Holder) { holder.rootView.setOnClickListener(null) holder.rootView.setOnLongClickListener(null) @@ -110,7 +165,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val titleView by bind(R.id.roomNameView) val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) - val lastEventView by bind(R.id.roomLastEventView) + val subtitleView by bind(R.id.subtitleView) val typingView by bind(R.id.roomTypingView) val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) @@ -120,6 +175,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val roomAvatarPublicDecorationImageView by bind(R.id.roomAvatarPublicDecorationImageView) val roomAvatarFailSendingImageView by bind(R.id.roomAvatarFailSendingImageView) val roomAvatarPresenceImageView by bind(R.id.roomAvatarPresenceImageView) - val rootView by bind(R.id.itemRoomLayout) + val rootView by bind(R.id.itemRoomLayout) } } 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 new file mode 100644 index 0000000000..8f2d949178 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemCentered.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +import android.view.HapticFeedbackConstants +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.amulyakhare.textdrawable.TextDrawable +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.ui.views.PresenceStateImageView +import im.vector.app.core.ui.views.ShieldImageView +import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.themes.ThemeUtils +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() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + lateinit var matrixItem: MatrixItem + + @EpoxyAttribute + var displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE + + @EpoxyAttribute + var encryptionTrustLevel: RoomEncryptionTrustLevel? = null + + @EpoxyAttribute + var userPresence: UserPresence? = null + + @EpoxyAttribute + var showPresence: Boolean = false + + @EpoxyAttribute @JvmField + var isPublic: Boolean = false + + @EpoxyAttribute + var unreadNotificationCount: Int = 0 + + @EpoxyAttribute + var hasUnreadMessage: Boolean = false + + @EpoxyAttribute + var hasDraft: Boolean = false + + @EpoxyAttribute + var hasFailedSending: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemLongClickListener: View.OnLongClickListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemClickListener: ClickListener? = null + + @EpoxyAttribute + var showSelected: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.rootView.onClick(itemClickListener) + holder.rootView.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + itemLongClickListener?.onLongClick(it) ?: false + } + holder.titleView.text = matrixItem.getBestName() + avatarRenderer.render(matrixItem, holder.avatarImageView) + holder.roomAvatarDecorationImageView.render(encryptionTrustLevel) + holder.roomAvatarPublicDecorationImageView.isVisible = isPublic + holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending + renderSelection(holder, showSelected) + holder.roomAvatarPresenceImageView.render(showPresence, userPresence) + } + + override fun unbind(holder: Holder) { + holder.rootView.setOnClickListener(null) + holder.rootView.setOnLongClickListener(null) + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + private fun renderSelection(holder: Holder, isSelected: Boolean) { + if (isSelected) { + holder.avatarCheckedImageView.visibility = View.VISIBLE + val backgroundColor = ThemeUtils.getColor(holder.view.context, R.attr.colorPrimary) + val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor) + holder.avatarImageView.setImageDrawable(backgroundDrawable) + } else { + holder.avatarCheckedImageView.visibility = View.GONE + avatarRenderer.render(matrixItem, holder.avatarImageView) + } + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.roomNameView) + val avatarCheckedImageView by bind(R.id.roomAvatarCheckedImageView) + val avatarImageView by bind(R.id.roomAvatarImageView) + val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView) + val roomAvatarPublicDecorationImageView by bind(R.id.roomAvatarPublicDecorationImageView) + val roomAvatarFailSendingImageView by bind(R.id.roomAvatarFailSendingImageView) + val roomAvatarPresenceImageView by bind(R.id.roomAvatarPresenceImageView) + val rootView by bind(R.id.itemRoomLayout) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 6326d9c97a..3f29c1d14c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence @@ -46,13 +47,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun create(roomSummary: RoomSummary, roomChangeMembershipStates: Map, selectedRoomIds: Set, + displayMode: RoomListDisplayMode, listener: RoomListListener?): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> { val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown createInvitationItem(roomSummary, changeMembershipState, listener) } - else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }) + else -> createRoomItem( + roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked } + ) } } @@ -105,9 +109,11 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun createRoomItem( roomSummary: RoomSummary, selectedRoomIds: Set, + displayMode: RoomListDisplayMode, onClick: ((RoomSummary) -> Unit)?, onLongClick: ((RoomSummary) -> Boolean)? ): VectorEpoxyModel<*> { + val subtitle = getSearchResultSubtitle(roomSummary) val unreadCount = roomSummary.notificationCount val showHighlighted = roomSummary.highlightCount > 0 val showSelected = selectedRoomIds.contains(roomSummary.roomId) @@ -118,28 +124,84 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) } + val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) - return RoomSummaryItem_() - .id(roomSummary.roomId) - .avatarRenderer(avatarRenderer) - // We do not display shield in the room list anymore - // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) - .izPublic(roomSummary.isPublic) - .showPresence(roomSummary.isDirect) - .userPresence(roomSummary.directUserPresence) - .matrixItem(roomSummary.toMatrixItem()) - .lastEventTime(latestEventTime) - .typingMessage(typingMessage) - .lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence()) - .showHighlighted(showHighlighted) - .showSelected(showSelected) - .hasFailedSending(roomSummary.hasFailedSending) - .unreadNotificationCount(unreadCount) - .hasUnreadMessage(roomSummary.hasUnreadMessages) - .hasDraft(roomSummary.userDrafts.isNotEmpty()) - .itemLongClickListener { _ -> - onLongClick?.invoke(roomSummary) ?: false - } - .itemClickListener { onClick?.invoke(roomSummary) } + + return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) { + createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) + } else { + createRoomSummaryItem( + roomSummary, displayMode, subtitle, latestEventTime, typingMessage, + latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick + ) + } + } + + private fun createRoomSummaryItem( + roomSummary: RoomSummary, + displayMode: RoomListDisplayMode, + subtitle: String, + latestEventTime: String, + typingMessage: String, + latestFormattedEvent: CharSequence, + showHighlighted: Boolean, + showSelected: Boolean, + unreadCount: Int, + onClick: ((RoomSummary) -> Unit)?, + onLongClick: ((RoomSummary) -> Boolean)? + ) = RoomSummaryItem_() + .id(roomSummary.roomId) + .avatarRenderer(avatarRenderer) + // We do not display shield in the room list anymore + // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) + .displayMode(displayMode) + .subtitle(subtitle) + .isPublic(roomSummary.isPublic) + .showPresence(roomSummary.isDirect) + .userPresence(roomSummary.directUserPresence) + .matrixItem(roomSummary.toMatrixItem()) + .lastEventTime(latestEventTime) + .typingMessage(typingMessage) + .lastFormattedEvent(latestFormattedEvent.toEpoxyCharSequence()) + .showHighlighted(showHighlighted) + .showSelected(showSelected) + .hasFailedSending(roomSummary.hasFailedSending) + .unreadNotificationCount(unreadCount) + .hasUnreadMessage(roomSummary.hasUnreadMessages) + .hasDraft(roomSummary.userDrafts.isNotEmpty()) + .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } + .itemClickListener { onClick?.invoke(roomSummary) } + + private fun createCenteredRoomSummaryItem( + roomSummary: RoomSummary, + displayMode: RoomListDisplayMode, + showSelected: Boolean, + unreadCount: Int, + onClick: ((RoomSummary) -> Unit)?, + onLongClick: ((RoomSummary) -> Boolean)? + ) = RoomSummaryItemCentered_() + .id(roomSummary.roomId) + .avatarRenderer(avatarRenderer) + // We do not display shield in the room list anymore + // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) + .displayMode(displayMode) + .isPublic(roomSummary.isPublic) + .showPresence(roomSummary.isDirect) + .userPresence(roomSummary.directUserPresence) + .matrixItem(roomSummary.toMatrixItem()) + .showSelected(showSelected) + .hasFailedSending(roomSummary.hasFailedSending) + .unreadNotificationCount(unreadCount) + .hasUnreadMessage(roomSummary.hasUnreadMessages) + .hasDraft(roomSummary.userDrafts.isNotEmpty()) + .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } + .itemClickListener { onClick?.invoke(roomSummary) } + + private fun getSearchResultSubtitle(roomSummary: RoomSummary): String { + val userId = roomSummary.directUserId + val spaceName = roomSummary.spaceParents?.firstOrNull()?.roomSummary?.name + val canonicalAlias = roomSummary.canonicalAlias + + return (userId ?: spaceName ?: canonicalAlias).orEmpty() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt index 75aaee45cb..2eb8921fd5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryListController.kt @@ -16,17 +16,19 @@ package im.vector.app.features.home.room.list +import im.vector.app.features.home.RoomListDisplayMode import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryListController( - private val roomSummaryItemFactory: RoomSummaryItemFactory + private val roomSummaryItemFactory: RoomSummaryItemFactory, + private val displayMode: RoomListDisplayMode ) : CollapsableTypedEpoxyController>() { var listener: RoomListListener? = null override fun buildModels(data: List?) { data?.forEach { - add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), listener)) + add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt index e9cbc69215..445438eec9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt @@ -19,11 +19,13 @@ package im.vector.app.features.home.room.list import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.app.core.utils.createUIHandler +import im.vector.app.features.home.RoomListDisplayMode import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary class RoomSummaryPagedController( - private val roomSummaryItemFactory: RoomSummaryItemFactory + private val roomSummaryItemFactory: RoomSummaryItemFactory, + private val displayMode: RoomListDisplayMode ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() @@ -57,6 +59,6 @@ class RoomSummaryPagedController( override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { // for place holder if enabled item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } - return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), listener) + return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt index c86d8ab243..f72698048d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedControllerFactory.kt @@ -16,18 +16,19 @@ package im.vector.app.features.home.room.list +import im.vector.app.features.home.RoomListDisplayMode import javax.inject.Inject class RoomSummaryPagedControllerFactory @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory ) { - fun createRoomSummaryPagedController(): RoomSummaryPagedController { - return RoomSummaryPagedController(roomSummaryItemFactory) + fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController { + return RoomSummaryPagedController(roomSummaryItemFactory, displayMode) } - fun createRoomSummaryListController(): RoomSummaryListController { - return RoomSummaryListController(roomSummaryItemFactory) + fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController { + return RoomSummaryListController(roomSummaryItemFactory, displayMode) } fun createSuggestedRoomListController(): SuggestedRoomListController { 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 new file mode 100644 index 0000000000..1ae017c98c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list + +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 = R.layout.item_space_directory_filter_no_results) +abstract class SpaceDirectoryFilterNoResults : VectorEpoxyModel() { + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index cad46e06a1..63cf666f30 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -28,8 +28,9 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.userdirectory.PendingSelection import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.onCompletion import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom @@ -55,39 +56,38 @@ class InviteUsersToRoomViewModel @AssistedInject constructor( } private fun inviteUsersToRoom(selections: Set) { - viewModelScope.launch { - _viewEvents.post(InviteUsersToRoomViewEvents.Loading) - selections.asFlow() - .map { user -> - when (user) { - is PendingSelection.UserPendingSelection -> room.membershipService().invite(user.user.userId, null) - is PendingSelection.ThreePidPendingSelection -> room.membershipService().invite3pid(user.threePid) - } + _viewEvents.post(InviteUsersToRoomViewEvents.Loading) + selections.asFlow() + .map { user -> + when (user) { + is PendingSelection.UserPendingSelection -> room.membershipService().invite(user.user.userId, null) + is PendingSelection.ThreePidPendingSelection -> room.membershipService().invite3pid(user.threePid) } - .catch { cause -> - _viewEvents.post(InviteUsersToRoomViewEvents.Failure(cause)) + }.onCompletion { error -> + if (error != null) return@onCompletion + + val successMessage = when (selections.size) { + 1 -> stringProvider.getString( + R.string.invitation_sent_to_one_user, + selections.first().getBestName() + ) + 2 -> stringProvider.getString( + R.string.invitations_sent_to_two_users, + selections.first().getBestName(), + selections.last().getBestName() + ) + else -> stringProvider.getQuantityString( + R.plurals.invitations_sent_to_one_and_more_users, + selections.size - 1, + selections.first().getBestName(), + selections.size - 1 + ) } - .collect { - val successMessage = when (selections.size) { - 1 -> stringProvider.getString( - R.string.invitation_sent_to_one_user, - selections.first().getBestName() - ) - 2 -> stringProvider.getString( - R.string.invitations_sent_to_two_users, - selections.first().getBestName(), - selections.last().getBestName() - ) - else -> stringProvider.getQuantityString( - R.plurals.invitations_sent_to_one_and_more_users, - selections.size - 1, - selections.first().getBestName(), - selections.size - 1 - ) - } - _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) - } - } + _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) + } + .catch { cause -> + _viewEvents.post(InviteUsersToRoomViewEvents.Failure(cause)) + }.launchIn(viewModelScope) } fun getUserIdsOfRoomMembers(): Set { diff --git a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt index 386e60359d..e453a347f5 100644 --- a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -136,7 +136,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager * Detect potential malicious activity. * Check if the activity running in app task is declared in app manifest. * - * @param activity the activity of the task + * @param activity the activity of the task * @return true if the activity is potentially malicious */ private fun isPotentialMaliciousActivity(activity: ComponentName): Boolean = activitiesInfo.none { it.name == activity.className } diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index 6f947290e2..c29e2e911a 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -22,5 +22,5 @@ const val DEFAULT_PIN_ID = "DEFAULT_PIN_ID" const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0 const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0 -const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds +const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 2 * 1_000L // every 2 seconds const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f diff --git a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt index 8f424af9ec..ec8638fdbb 100644 --- a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt @@ -17,20 +17,10 @@ package im.vector.app.features.location import android.app.Activity -import im.vector.app.core.utils.openAppSettingsPage class DefaultLocationSharingNavigator constructor(val activity: Activity?) : LocationSharingNavigator { - override var goingToAppSettings: Boolean = false - override fun quit() { activity?.finish() } - - override fun goToAppSettings() { - activity?.let { - goingToAppSettings = true - openAppSettingsPage(it) - } - } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt index 061f338e72..b3466ff871 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -29,7 +29,7 @@ data class LocationData( ) : Parcelable /** - * Creates location data from a LocationContent. + * Creates location data from a MessageLocationContent. * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) * @return location data or null if geo uri is not valid */ @@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? { return parseGeo(getBestGeoUri()) } +/** + * Creates location data from a geoUri String. + * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) + * @return location data or null if geo uri is null or not valid + */ +fun String?.toLocationData(): LocationData? { + return this?.let { parseGeo(it) } +} + @VisibleForTesting fun parseGeo(geo: String): LocationData? { val geoParts = geo 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 e472c568b6..89aedf3aa9 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 @@ -32,16 +32,16 @@ import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentLocationSharingBinding -import im.vector.app.features.VectorFeatures import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.location.live.duration.ChooseLiveDurationBottomSheet import im.vector.app.features.location.option.LocationSharingOption +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.util.MatrixItem import java.lang.ref.WeakReference import javax.inject.Inject @@ -53,7 +53,7 @@ class LocationSharingFragment @Inject constructor( private val urlMapProvider: UrlMapProvider, private val avatarRenderer: AvatarRenderer, private val matrixItemColorProvider: MatrixItemColorProvider, - private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, ) : VectorBaseFragment(), LocationTargetChangeListener, VectorBaseBottomSheetDialogFragment.ResultListener { @@ -100,11 +100,6 @@ class LocationSharingFragment @Inject constructor( override fun onResume() { super.onResume() views.mapView.onResume() - if (locationSharingNavigator.goingToAppSettings) { - locationSharingNavigator.goingToAppSettings = false - // retry to start live location - tryStartLiveLocationSharing() - } } override fun onPause() { @@ -162,18 +157,6 @@ class LocationSharingFragment @Inject constructor( .show() } - private fun handleMissingBackgroundLocationPermission() { - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.location_in_background_missing_permission_dialog_title) - .setMessage(R.string.location_in_background_missing_permission_dialog_content) - .setPositiveButton(R.string.settings) { _, _ -> - locationSharingNavigator.goToAppSettings() - } - .setNegativeButton(R.string.action_not_now, null) - .setCancelable(false) - .show() - } - private fun initLocateButton() { views.mapView.locateButton.setOnClickListener { viewModel.handle(LocationSharingAction.ZoomToUserLocation) @@ -181,7 +164,7 @@ class LocationSharingFragment @Inject constructor( } private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) { - views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude) + views.mapView.zoomToLocation(event.userLocation) } private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) { @@ -212,30 +195,16 @@ class LocationSharingFragment @Inject constructor( } private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted && checkPermissions(PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, requireActivity(), backgroundLocationResultLauncher)) { - startLiveLocationSharing() - } else if (deniedPermanently) { - handleMissingBackgroundLocationPermission() - } - } - - private val backgroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { startLiveLocationSharing() } else if (deniedPermanently) { - handleMissingBackgroundLocationPermission() + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) } } private fun tryStartLiveLocationSharing() { // we need to re-check foreground location to be sure it has not changed after landing on this screen - if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher) && - checkPermissions( - PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, - requireActivity(), - backgroundLocationResultLauncher, - R.string.location_in_background_missing_permission_dialog_content - )) { + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) { startLiveLocationSharing() } } @@ -255,7 +224,7 @@ class LocationSharingFragment @Inject constructor( // first, update the options view val options: Set = when (state.areTargetAndUserLocationEqual) { true -> { - if (vectorFeatures.isLiveLocationEnabled()) { + if (vectorPreferences.labsEnableLiveLocation()) { setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) } else { setOf(LocationSharingOption.USER_CURRENT) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt index 8927da9239..6f4759a1e4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt @@ -17,7 +17,5 @@ package im.vector.app.features.location interface LocationSharingNavigator { - var goingToAppSettings: Boolean fun quit() - fun goToAppSettings() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 362b82ccf5..8b9a1c75ae 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -55,7 +55,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { private val binder = LocalBinder() - private var roomArgsList = mutableListOf() + /** + * Keep track of a map between beacon event Id starting the live and RoomArgs. + */ + private var roomArgsMap = mutableMapOf() private var timers = mutableListOf() override fun onCreate() { @@ -73,8 +76,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}") if (roomArgs != null) { - roomArgsList.add(roomArgs) - // Show a sticky notification val notification = notificationUtils.buildLiveLocationSharingNotification() startForeground(roomArgs.roomId.hashCode(), notification) @@ -87,7 +88,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { .getSafeActiveSession() ?.let { session -> session.coroutineScope.launch(session.coroutineDispatchers.io) { - sendLiveBeaconInfo(session, roomArgs) + sendStartingLiveBeaconInfo(session, roomArgs) } } } @@ -95,7 +96,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { return START_STICKY } - private suspend fun sendLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { + private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { val beaconContent = MessageBeaconInfoContent( timeout = roomArgs.durationMillis, isLive = true, @@ -103,7 +104,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { ).toContent() val stateKey = session.myUserId - session + val beaconEventId = session .getRoom(roomArgs.roomId) ?.stateService() ?.sendStateEvent( @@ -111,6 +112,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { stateKey = stateKey, body = beaconContent ) + + beaconEventId + ?.takeUnless { it.isEmpty() } + ?.let { + roomArgsMap[it] = roomArgs + locationTracker.requestLastKnownLocation() + } + ?: run { + Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id") + } } private fun scheduleTimer(roomId: String, durationMillis: Long) { @@ -134,9 +145,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { // Send a new beacon info state by setting live field as false sendStoppedBeaconInfo(roomId) - synchronized(roomArgsList) { - roomArgsList.removeAll { it.roomId == roomId } - if (roomArgsList.isEmpty()) { + synchronized(roomArgsMap) { + val beaconIds = roomArgsMap + .filter { it.value.roomId == roomId } + .map { it.key } + beaconIds.forEach { roomArgsMap.remove(it) } + + if (roomArgsMap.isEmpty()) { Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") destroyMe() } @@ -156,16 +171,17 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { override fun onLocationUpdate(locationData: LocationData) { Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") - val session = activeSessionHolder.getSafeActiveSession() // Emit location update to all rooms in which live location sharing is active - session?.coroutineScope?.launch(session.coroutineDispatchers.io) { - roomArgsList.toList().forEach { roomArg -> - sendLiveLocation(roomArg.roomId, locationData) - } + roomArgsMap.toMap().forEach { item -> + sendLiveLocation(item.value.roomId, item.key, locationData) } } - private suspend fun sendLiveLocation(roomId: String, locationData: LocationData) { + private fun sendLiveLocation( + roomId: String, + beaconInfoEventId: String, + locationData: LocationData + ) { val session = activeSessionHolder.getSafeActiveSession() val room = session?.getRoom(roomId) val userId = session?.myUserId @@ -174,18 +190,12 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { return } - room - .stateService() - .getLiveLocationBeaconInfo(userId, true) - ?.eventId - ?.let { - room.sendService().sendLiveLocation( - beaconInfoEventId = it, - latitude = locationData.latitude, - longitude = locationData.longitude, - uncertainty = locationData.uncertainty - ) - } + room.sendService().sendLiveLocation( + beaconInfoEventId = beaconInfoEventId, + latitude = locationData.latitude, + longitude = locationData.longitude, + uncertainty = locationData.uncertainty + ) } override fun onLocationProviderIsNotAvailable() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index b7006370a6..4e56e7954c 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -40,10 +40,12 @@ class LocationTracker @Inject constructor( fun onLocationProviderIsNotAvailable() } - private var callbacks = mutableListOf() + private val callbacks = mutableListOf() private var hasGpsProviderLiveLocation = false + private var lastLocation: LocationData? = null + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { Timber.d("## LocationTracker. start()") @@ -92,6 +94,14 @@ class LocationTracker @Inject constructor( callbacks.clear() } + /** + * Request the last known location. It will be given async through Callback. + * Please ensure adding a callback to receive the value. + */ + fun requestLastKnownLocation() { + lastLocation?.let { location -> callbacks.forEach { it.onLocationUpdate(location) } } + } + fun addCallback(callback: Callback) { if (!callbacks.contains(callback)) { callbacks.add(callback) @@ -127,7 +137,9 @@ class LocationTracker @Inject constructor( } } } - callbacks.forEach { it.onLocationUpdate(location.toLocationData()) } + val locationData = location.toLocationData() + lastLocation = locationData + callbacks.forEach { it.onLocationUpdate(locationData) } } override fun onProviderDisabled(provider: String) { diff --git a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt new file mode 100644 index 0000000000..cbfdf1dfda --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.constants.MapboxConstants +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.mapbox.mapboxsdk.maps.MapboxMap + +fun MapboxMap?.zoomToLocation(locationData: LocationData, preserveCurrentZoomLevel: Boolean = false) { + val zoomLevel = if (preserveCurrentZoomLevel && this?.cameraPosition != null) { + cameraPosition.zoom + } else { + INITIAL_MAP_ZOOM_IN_PREVIEW + } + this?.cameraPosition = CameraPosition.Builder() + .target(LatLng(locationData.latitude, locationData.longitude)) + .zoom(zoomLevel) + .build() +} + +fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) { + this?.getCameraForLatLngBounds(latLngBounds)?.let { camPosition -> + // unZoom a little to avoid having pins exactly at the edges of the map + cameraPosition = CameraPosition.Builder(camPosition) + .zoom((camPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble())) + .build() + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index 69e4e9fc20..dd2a56fb3a 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -25,7 +25,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.marginBottom import androidx.core.view.marginTop import androidx.core.view.updateLayoutParams -import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapboxMap @@ -164,7 +163,7 @@ class MapTilerMapView @JvmOverloads constructor( state.userLocationData?.let { locationData -> if (!initZoomDone || !state.zoomOnlyOnce) { - zoomToLocation(locationData.latitude, locationData.longitude) + zoomToLocation(locationData) initZoomDone = true } @@ -180,12 +179,9 @@ class MapTilerMapView @JvmOverloads constructor( } } - fun zoomToLocation(latitude: Double, longitude: Double) { + fun zoomToLocation(locationData: LocationData) { Timber.d("## Location: zoomToLocation") - mapRefs?.map?.cameraPosition = CameraPosition.Builder() - .target(LatLng(latitude, longitude)) - .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW) - .build() + mapRefs?.map?.zoomToLocation(locationData) } fun getLocationOfMapCenter(): LocationData? = diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt new file mode 100644 index 0000000000..8cb552e3c4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.live + +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.os.CountDownTimer +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.isVisible +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import im.vector.app.R +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.TextUtils +import im.vector.app.databinding.ViewLocationLiveMessageBannerBinding +import im.vector.app.features.themes.ThemeUtils +import org.threeten.bp.Duration + +private const val REMAINING_TIME_COUNTER_INTERVAL_IN_MS = 1000L + +class LocationLiveMessageBannerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewLocationLiveMessageBannerBinding.inflate( + LayoutInflater.from(context), + this + ) + + val stopButton: Button + get() = binding.locationLiveMessageBannerStop + + private val background: ImageView + get() = binding.locationLiveMessageBannerBackground + + private val title: TextView + get() = binding.locationLiveMessageBannerTitle + + private val subTitle: TextView + get() = binding.locationLiveMessageBannerSubTitle + + private var countDownTimer: CountDownTimer? = null + + fun render(viewState: LocationLiveMessageBannerViewState) { + when (viewState) { + is LocationLiveMessageBannerViewState.Emitter -> renderEmitter(viewState) + is LocationLiveMessageBannerViewState.Watcher -> renderWatcher(viewState) + } + + GlideApp.with(context) + .load(ColorDrawable(ThemeUtils.getColor(context, android.R.attr.colorBackground))) + .transform(GranularRoundedCorners(0f, 0f, viewState.bottomEndCornerRadiusInDp, viewState.bottomStartCornerRadiusInDp)) + .into(background) + } + + private fun renderEmitter(viewState: LocationLiveMessageBannerViewState.Emitter) { + stopButton.isVisible = true + title.text = context.getString(R.string.location_share_live_enabled) + + countDownTimer?.cancel() + viewState.remainingTimeInMillis + .takeIf { it >= 0 } + ?.let { + countDownTimer = object : CountDownTimer(it, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) { + override fun onTick(millisUntilFinished: Long) { + val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L)) + subTitle.text = context.getString( + R.string.location_share_live_remaining_time, + TextUtils.formatDurationWithUnits(context, duration) + ) + } + + override fun onFinish() { + subTitle.text = context.getString( + R.string.location_share_live_remaining_time, + TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L)) + ) + } + } + countDownTimer?.start() + } + + val rootLayout: ConstraintLayout? = (binding.root as? ConstraintLayout) + rootLayout?.let { parentLayout -> + val constraintSet = ConstraintSet() + constraintSet.clone(rootLayout) + + if (viewState.isStopButtonCenteredVertically) { + constraintSet.connect( + R.id.locationLiveMessageBannerStop, + ConstraintSet.BOTTOM, + R.id.locationLiveMessageBannerBackground, + ConstraintSet.BOTTOM, + 0 + ) + } else { + constraintSet.clear(R.id.locationLiveMessageBannerStop, ConstraintSet.BOTTOM) + } + + constraintSet.applyTo(parentLayout) + } + } + + private fun renderWatcher(viewState: LocationLiveMessageBannerViewState.Watcher) { + stopButton.isVisible = false + title.text = context.getString(R.string.location_share_live_view) + subTitle.text = context.getString(R.string.location_share_live_until, viewState.formattedLocalTimeOfEndOfLive) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt new file mode 100644 index 0000000000..976085386b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt @@ -0,0 +1,36 @@ +/* + * 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.location.live + +sealed class LocationLiveMessageBannerViewState( + open val bottomStartCornerRadiusInDp: Float, + open val bottomEndCornerRadiusInDp: Float, +) { + + data class Emitter( + override val bottomStartCornerRadiusInDp: Float, + override val bottomEndCornerRadiusInDp: Float, + val remainingTimeInMillis: Long, + val isStopButtonCenteredVertically: Boolean + ) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp) + + data class Watcher( + override val bottomStartCornerRadiusInDp: Float, + override val bottomEndCornerRadiusInDp: Float, + val formattedLocalTimeOfEndOfLive: String, + ) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp) +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt new file mode 100644 index 0000000000..91f6999e2c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt @@ -0,0 +1,40 @@ +/* + * 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.location.live.map + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapLatest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import javax.inject.Inject + +class GetListOfUserLiveLocationUseCase @Inject constructor( + private val session: Session, + private val userLiveLocationViewStateMapper: UserLiveLocationViewStateMapper, +) { + + fun execute(roomId: String): Flow> { + return session.getRoom(roomId) + ?.locationSharingService() + ?.getRunningLiveLocationShareSummaries() + ?.asFlow() + ?.mapLatest { it.mapNotNull { summary -> userLiveLocationViewStateMapper.map(summary) } } + ?: emptyFlow() + } +} 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 new file mode 100644 index 0000000000..c07104e72c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationBottomSheetController.kt @@ -0,0 +1,89 @@ +/* + * 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.location.live.map + +import android.content.Context +import com.airbnb.epoxy.EpoxyController +import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.DateProvider +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( + private val avatarRenderer: AvatarRenderer, + private val vectorDateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider, + private val clock: Clock, + private val context: Context, +) : EpoxyController() { + + interface Callback { + fun onUserSelected(userId: String) + fun onStopLocationClicked() + } + + private var userLocations: List? = null + var callback: Callback? = null + + fun setData(userLocations: List) { + this.userLocations = userLocations + requestModelBuild() + } + + override fun buildModels() { + val currentUserLocations = userLocations ?: return + val host = this + + val userItemCallback = object : LiveLocationUserItem.Callback { + override fun onUserSelected(userId: String) { + host.callback?.onUserSelected(userId) + } + + override fun onStopSharingClicked() { + host.callback?.onStopLocationClicked() + } + } + + currentUserLocations.forEach { liveLocationViewState -> + val remainingTime = getFormattedLocalTimeEndOfLive(liveLocationViewState.endOfLiveTimestampMillis) + liveLocationUserItem { + id(liveLocationViewState.matrixItem.id) + callback(userItemCallback) + matrixItem(liveLocationViewState.matrixItem) + stringProvider(host.stringProvider) + clock(host.clock) + avatarRenderer(host.avatarRenderer) + remainingTime(remainingTime) + locationUpdateTimeMillis(liveLocationViewState.locationTimestampMillis) + showStopSharingButton(liveLocationViewState.showStopSharingButton) + } + } + } + + private fun getFormattedLocalTimeEndOfLive(endOfLiveDateTimestampMillis: Long?): String { + val endOfLiveDateTime = DateProvider.toLocalDateTime(endOfLiveDateTimestampMillis) + val formattedDateTime = endOfLiveDateTime.toTimestamp().let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) } + return stringProvider.getString(R.string.location_share_live_until, formattedDateTime) + } +} 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 new file mode 100644 index 0000000000..336f688434 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.live.map.bottomsheet + +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock +import im.vector.app.core.utils.TextUtils +import im.vector.app.features.home.AvatarRenderer +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() { + + interface Callback { + fun onUserSelected(userId: String) + fun onStopSharingClicked() + } + + @EpoxyAttribute + var callback: Callback? = null + + @EpoxyAttribute + lateinit var matrixItem: MatrixItem + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + lateinit var stringProvider: StringProvider + + @EpoxyAttribute + lateinit var clock: Clock + + @EpoxyAttribute + var remainingTime: String? = null + + @EpoxyAttribute + var locationUpdateTimeMillis: Long? = null + + @EpoxyAttribute + var showStopSharingButton: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + avatarRenderer.render(matrixItem, holder.itemUserAvatarImageView) + holder.itemUserDisplayNameTextView.text = matrixItem.displayName + holder.itemRemainingTimeTextView.text = remainingTime + + holder.itemStopSharingButton.isVisible = showStopSharingButton + if (showStopSharingButton) { + holder.itemStopSharingButton.onClick { + callback?.onStopSharingClicked() + } + } + + stopTimer(holder) + holder.timer = CountUpTimer(1000).apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis) + } + } + resume() + } + + holder.view.setOnClickListener { callback?.onUserSelected(matrixItem.id) } + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + stopTimer(holder) + } + + private fun stopTimer(holder: Holder) { + holder.timer?.stop() + holder.timer = null + } + + private fun getFormattedLastUpdatedAt(locationUpdateTimeMillis: Long?): String { + if (locationUpdateTimeMillis == null) return "" + val elapsedTime = clock.epochMillis() - locationUpdateTimeMillis + val duration = Duration.ofMillis(elapsedTime.coerceAtLeast(0L)) + val formattedDuration = TextUtils.formatDurationWithUnits(stringProvider, duration, appendSeconds = false) + return stringProvider.getString(R.string.live_location_bottom_sheet_last_updated_at, formattedDuration) + } + + class Holder : VectorEpoxyHolder() { + var timer: CountUpTimer? = null + val itemUserAvatarImageView by bind(R.id.itemUserAvatarImageView) + val itemUserDisplayNameTextView by bind(R.id.itemUserDisplayNameTextView) + val itemRemainingTimeTextView by bind(R.id.itemRemainingTimeTextView) + val itemLastUpdatedAtTextView by bind(R.id.itemLastUpdatedAtTextView) + val itemStopSharingButton by bind