diff --git a/CHANGES.md b/CHANGES.md index c8677b1ae4..4ff45e9c62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,7 +36,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 fe71865ef1..023f7a909c 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" diff --git a/changelog.d/5278.wip b/changelog.d/5278.wip new file mode 100644 index 0000000000..c6014dc9ac --- /dev/null +++ b/changelog.d/5278.wip @@ -0,0 +1 @@ +Adds email input and verification screens to the new FTUE onboarding flow diff --git a/changelog.d/5283.wip b/changelog.d/5283.wip new file mode 100644 index 0000000000..1c2fbfcd61 --- /dev/null +++ b/changelog.d/5283.wip @@ -0,0 +1 @@ +FTUE - Adds the redesigned Sign In screen diff --git a/changelog.d/5783.wip b/changelog.d/5783.wip new file mode 100644 index 0000000000..6f44813d81 --- /dev/null +++ b/changelog.d/5783.wip @@ -0,0 +1 @@ +FTUE - Overrides sign up flow ordering for matrix.org only diff --git a/changelog.d/5856.bugfix b/changelog.d/5856.bugfix new file mode 100644 index 0000000000..87f10ac9b2 --- /dev/null +++ b/changelog.d/5856.bugfix @@ -0,0 +1 @@ +Use fixed text size in read receipt counter diff --git a/changelog.d/6012.wip b/changelog.d/6012.wip new file mode 100644 index 0000000000..9c67d562fe --- /dev/null +++ b/changelog.d/6012.wip @@ -0,0 +1 @@ +Live location sharing: navigation from timeline to map screen diff --git a/changelog.d/6032.bugfix b/changelog.d/6032.bugfix new file mode 100644 index 0000000000..c20d7ddd08 --- /dev/null +++ b/changelog.d/6032.bugfix @@ -0,0 +1 @@ +Revert: Use member name instead of room name in DM creation item diff --git a/changelog.d/6074.bugfix b/changelog.d/6074.bugfix new file mode 100644 index 0000000000..692dce28d7 --- /dev/null +++ b/changelog.d/6074.bugfix @@ -0,0 +1 @@ +Poll refactoring with unit tests diff --git a/changelog.d/6077.sdk b/changelog.d/6077.sdk new file mode 100644 index 0000000000..80310a28f5 --- /dev/null +++ b/changelog.d/6077.sdk @@ -0,0 +1 @@ +Improve replay attacks and reduce duplicate message index errors diff --git a/changelog.d/6089.misc b/changelog.d/6089.misc new file mode 100644 index 0000000000..19b951c1a3 --- /dev/null +++ b/changelog.d/6089.misc @@ -0,0 +1 @@ +Test: Ensure calling 'fail()' is not caught by the catch block diff --git a/changelog.d/6098.feature b/changelog.d/6098.feature new file mode 100644 index 0000000000..659da42094 --- /dev/null +++ b/changelog.d/6098.feature @@ -0,0 +1 @@ +Labs flag for enabling live location sharing diff --git a/changelog.d/6100.misc b/changelog.d/6100.misc new file mode 100644 index 0000000000..2fb5ecf34d --- /dev/null +++ b/changelog.d/6100.misc @@ -0,0 +1 @@ +Excludes transitive optional non FOSS google location dependency from fdroid builds diff --git a/changelog.d/6103.bugfix b/changelog.d/6103.bugfix new file mode 100644 index 0000000000..12e6836460 --- /dev/null +++ b/changelog.d/6103.bugfix @@ -0,0 +1 @@ +Glide - Use current drawable while loading new static map image diff --git a/changelog.d/6109.bugfix b/changelog.d/6109.bugfix new file mode 100644 index 0000000000..43b1d610c7 --- /dev/null +++ b/changelog.d/6109.bugfix @@ -0,0 +1 @@ +Fix sending multiple invites to a room reaching only one or two people diff --git a/changelog.d/6123.wip b/changelog.d/6123.wip new file mode 100644 index 0000000000..680498280f --- /dev/null +++ b/changelog.d/6123.wip @@ -0,0 +1 @@ +[Live location sharing] Update entity in DB when a live is timed out diff --git a/changelog.d/6140.bugfix b/changelog.d/6140.bugfix new file mode 100644 index 0000000000..247e69f837 --- /dev/null +++ b/changelog.d/6140.bugfix @@ -0,0 +1 @@ +Prevent widget web view from reloading on screen / orientation change diff --git a/changelog.d/6141.misc b/changelog.d/6141.misc new file mode 100644 index 0000000000..2f0a91b451 --- /dev/null +++ b/changelog.d/6141.misc @@ -0,0 +1 @@ +Downgrade gradle from 7.2.0 to 7.1.3 diff --git a/changelog.d/6148.bugfix b/changelog.d/6148.bugfix new file mode 100644 index 0000000000..3aa623315a --- /dev/null +++ b/changelog.d/6148.bugfix @@ -0,0 +1 @@ +Fix decrypting redacted event from sending errors diff --git a/dependencies.gradle b/dependencies.gradle index 36d7e425ac..1d6bfcd3a5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,7 +7,10 @@ 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" @@ -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.13.0" // Testing def mockk = "1.12.4" @@ -51,7 +54,7 @@ ext.libs = [ '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", @@ -110,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/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_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/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/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/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 6678b109a6..96ea99d92f 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 @@ -96,7 +96,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 +119,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 +142,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 +207,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 +232,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, @@ -251,8 +251,8 @@ class CommonTestHelper(context: Context) { /** * 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 */ @@ -267,8 +267,8 @@ class CommonTestHelper(context: Context) { /** * 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, @@ -305,8 +305,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, 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/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..de5fa41581 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -0,0 +1,79 @@ +/* + * 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 +import org.matrix.android.sdk.common.CryptoTestHelper + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class DecryptRedactedEventTest : InstrumentedTest { + + @Test + fun doNotFailToDecryptRedactedEvent() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = 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 38597269cb..74c218af58 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,7 @@ 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 @@ -62,11 +63,13 @@ 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) @@ -525,10 +528,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) { } } 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 1bd2a46381..895f95aeac 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,7 +21,6 @@ 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 @@ -47,6 +46,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper 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) @@ -95,12 +95,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() @@ -168,12 +166,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 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 1c3c6c46e7..13133b726c 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 @@ -42,6 +42,7 @@ 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) @@ -96,17 +97,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.UNVERIFIED.value, technicalMessage) } // Let's see if the reply we got from bob first session is unverified @@ -137,17 +140,18 @@ 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.UNVERIFIED.value, technicalMessage) } testHelper.signOutAndClose(aliceSession) @@ -190,17 +194,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 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..d5fd299f1f 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 @@ -45,6 +46,7 @@ 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.RetryTestRule import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestMatrixCallback import java.util.Collections @@ -55,6 +57,8 @@ 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 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..09c340a14f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -0,0 +1,115 @@ +/* + * 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 +import org.matrix.android.sdk.common.CryptoTestHelper + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class ReplayAttackTest : InstrumentedTest { + + @Test + fun replayAttackAlreadyDecryptedEventTest() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = 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() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = 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") + } + } + cryptoTestData.cleanUp(testHelper) + } +} 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/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..9029c7f8a3 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, @@ -173,12 +173,12 @@ interface KeysBackupService { /** * 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?, 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/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/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index 5f9857eb2f..5cb7857021 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. 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/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/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt index 0b28d62f56..059fe21471 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,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati * Aggregation info concerning a live location share. */ data class LiveLocationShareAggregatedSummary( + /** + * 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/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 d05fdb951f..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 @@ -90,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 */ 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..e3a9860523 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) 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/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/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..813adf7459 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 @@ -1105,6 +1105,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 */ 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/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/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/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt index 8538e5a5af..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, 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/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index 4d0d2c5c64..e84337693f 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,9 @@ internal open class LiveLocationShareAggregatedSummaryEntity( var roomId: 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..816b5f4392 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 @@ -55,3 +55,11 @@ 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() +} 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 b5f49d7f9c..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,6 +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.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 @@ -385,4 +387,7 @@ internal abstract class SessionModule { @Binds abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor + + @Binds + 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/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/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/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index af9c0071fe..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,12 +176,16 @@ 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 -> { @@ -245,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 -> { @@ -318,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 }) { @@ -363,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) @@ -376,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?, @@ -392,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, 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/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/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 76b7a4ec8e..42dfc7ba9f 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,6 +16,7 @@ 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 @@ -26,17 +27,27 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati 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 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 class LiveLocationAggregationProcessor @Inject constructor() { +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 targetEventId = if (content.isLive.orTrue()) { + 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 @@ -56,8 +67,39 @@ internal class LiveLocationAggregationProcessor @Inject constructor() { Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}") - aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } - aggregatedSummary.isActive = content.isLive + val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } + aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis + aggregatedSummary.isActive = isLive + + 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( 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/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/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/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/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/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/build.gradle b/vector/build.gradle index 99ced285cc..766da4c321 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -505,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/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/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..8c2e25bc7e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -306,7 +306,9 @@ android:supportsPictureInPicture="true" /> - + + @@ -343,6 +345,7 @@ + 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/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/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/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/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/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/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..a41abba7ab 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 @@ -101,9 +101,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 +145,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/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/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/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 635b00c05d..f0cfff4cf8 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 @@ -221,6 +221,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 +648,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 +714,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() } @@ -2015,6 +2023,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/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt index 479a742369..3c7b6c32e1 100644 --- 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 @@ -33,7 +33,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocation 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.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.threeten.bp.LocalDateTime @@ -75,7 +74,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor( val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) return MessageLiveLocationInactiveItem_() - .attributes(attributes) + // disable the click on this state item + .attributes(attributes.copy(itemClickListener = null)) .mapWidth(width) .mapHeight(height) .highlighted(highlight) @@ -90,7 +90,8 @@ class LiveLocationShareMessageItemFactory @Inject constructor( val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) return MessageLiveLocationStartItem_() - .attributes(attributes) + // disable the click on this state item + .attributes(attributes.copy(itemClickListener = null)) .mapWidth(width) .mapHeight(height) .highlighted(highlight) @@ -127,7 +128,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor( private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState { return when { liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown - liveLocationShareSummaryData.isActive.not() || isLiveTimedOut(liveLocationShareSummaryData) -> LiveLocationShareViewState.Inactive + liveLocationShareSummaryData.isActive.not() -> LiveLocationShareViewState.Inactive liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading else -> LiveLocationShareViewState.Running( @@ -137,16 +138,6 @@ class LiveLocationShareMessageItemFactory @Inject constructor( }.also { viewState -> Timber.d("computed viewState: $viewState") } } - private fun isLiveTimedOut(liveLocationShareSummaryData: LiveLocationShareSummaryData): Boolean { - return getEndOfLiveDateTime(liveLocationShareSummaryData) - ?.let { endOfLive -> - // this will only cover users with different timezones but not users with manually time set - val now = LocalDateTime.now() - now.isAfter(endOfLive) - } - .orFalse() - } - private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? { return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) } } 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/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/AbsMessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt index f7146c24e9..e7823845fa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt @@ -75,6 +75,7 @@ abstract class AbsMessageLocationItem : AbsMe GlideApp.with(holder.staticMapImageView) .load(location) .apply(RequestOptions.centerCropTransform()) + .placeholder(holder.staticMapImageView.drawable) .listener(object : RequestListener { override fun onLoadFailed(e: GlideException?, model: Any?, 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) { - 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/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index e472c568b6..cc5586e7f5 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 @@ -37,11 +37,11 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING import im.vector.app.core.utils.checkPermissions 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 { @@ -255,7 +255,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/live/map/LocationLiveMapViewActivity.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewActivity.kt new file mode 100644 index 0000000000..c0f07dba57 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewActivity.kt @@ -0,0 +1,68 @@ +/* + * 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 android.content.Intent +import android.os.Parcelable +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLocationSharingBinding +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocationLiveMapViewArgs( + val roomId: String +) : Parcelable + +@AndroidEntryPoint +class LocationLiveMapViewActivity : VectorBaseActivity() { + + override fun getBinding() = ActivityLocationSharingBinding.inflate(layoutInflater) + + override fun initUiAndData() { + val mapViewArgs: LocationLiveMapViewArgs? = intent?.extras?.getParcelable(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS) + if (mapViewArgs == null) { + finish() + return + } + setupToolbar(views.toolbar) + .setTitle(getString(R.string.location_activity_title_preview)) + .allowBack() + + if (isFirstCreation()) { + addFragment( + views.fragmentContainer, + LocationLiveMapViewFragment::class.java, + mapViewArgs + ) + } + } + + companion object { + + private const val EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS = "EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS" + + fun getIntent(context: Context, locationLiveMapViewArgs: LocationLiveMapViewArgs): Intent { + return Intent(context, LocationLiveMapViewActivity::class.java).apply { + putExtra(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS, locationLiveMapViewArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt new file mode 100644 index 0000000000..32b87727d8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.args +import com.mapbox.mapboxsdk.maps.MapboxMapOptions +import com.mapbox.mapboxsdk.maps.SupportMapFragment +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.addChildFragment +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSimpleContainerBinding +import im.vector.app.features.location.UrlMapProvider +import javax.inject.Inject + +/** + * Screen showing a map with all the current users sharing their live location in room. + */ +@AndroidEntryPoint +class LocationLiveMapViewFragment : VectorBaseFragment() { + + @Inject + lateinit var urlMapProvider: UrlMapProvider + + private val args: LocationLiveMapViewArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding { + return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupMap() + } + + private fun setupMap() { + val mapFragment = getOrCreateSupportMapFragment() + + mapFragment.getMapAsync { mapBoxMap -> + lifecycleScope.launchWhenCreated { + mapBoxMap.setStyle(urlMapProvider.getMapUrl()) + } + } + } + + private fun getOrCreateSupportMapFragment() = + childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment + ?: run { + val options = MapboxMapOptions.createFromAttributes(requireContext(), null) + SupportMapFragment.newInstance(options) + .also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) } + } + + companion object { + private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map" + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 68fc2d1c59..49fa815a56 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -159,3 +159,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt() } } + +fun SocialLoginButtonsView.render(ssoProviders: List?, mode: SocialLoginButtonsView.Mode, listener: (String?) -> Unit) { + this.mode = mode + this.ssoIdentityProviders = ssoProviders?.sorted() + this.listener = SocialLoginButtonsView.InteractionListener { listener(it) } +} diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 0f921ab80a..a051266688 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -68,6 +68,8 @@ import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationSharingActivity import im.vector.app.features.location.LocationSharingArgs import im.vector.app.features.location.LocationSharingMode +import im.vector.app.features.location.live.map.LocationLiveMapViewActivity +import im.vector.app.features.location.live.map.LocationLiveMapViewArgs import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.matrixto.MatrixToBottomSheet @@ -592,6 +594,14 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openLocationLiveMap(context: Context, roomId: String) { + val intent = LocationLiveMapViewActivity.getIntent( + context = context, + locationLiveMapViewArgs = LocationLiveMapViewArgs(roomId = roomId) + ) + context.startActivity(intent) + } + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { if (buildTask) { val stackBuilder = TaskStackBuilder.create(context) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 075b41daf3..d4ef2b8099 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -172,6 +172,8 @@ interface Navigator { initialLocationData: LocationData?, locationOwnerId: String?) + fun openLocationLiveMap(context: Context, roomId: String) + fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index d03fcadcfa..abfca1a64c 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -233,6 +233,7 @@ class NotificationUtils @Inject constructor( * Build a polling thread listener notification. * * @param subTitleResId subtitle string resource Id of the notification + * @param withProgress true to show indeterminate progress on the notification * @return the polling thread listener notification */ @SuppressLint("NewApi") @@ -298,10 +299,8 @@ class NotificationUtils @Inject constructor( * Build an incoming call notification. * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. * - * @param isVideo true if this is a video call, false for voice call - * @param roomName the room name in which the call is pending. - * @param matrixId the matrix id - * @param callId the call id. + * @param call information about the call + * @param title title of the notification * @param fromBg true if the app is in background when posting the notification * @return the call notification. */ @@ -430,11 +429,8 @@ class NotificationUtils @Inject constructor( /** * Build a pending call notification. * - * @param isVideo true if this is a video call, false for voice call - * @param roomName the room name in which the call is pending. - * @param roomId the room Id - * @param matrixId the matrix id - * @param callId the call id. + * @param call information about the call + * @param title title of the notification * @return the call notification. */ @SuppressLint("NewApi") diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt index 3014b199b4..925c838d80 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.onboarding import im.vector.app.R import im.vector.app.core.extensions.andThen import im.vector.app.core.resources.StringProvider -import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister +import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction.LoginDirect import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -33,8 +33,8 @@ class DirectLoginUseCase @Inject constructor( private val uriFactory: UriFactory ) { - suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result { - return fetchWellKnown(action.username, homeServerConnectionConfig) + suspend fun execute(action: LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?): Result { + return fetchWellKnown(action.matrixId, homeServerConnectionConfig) .andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) } } @@ -42,13 +42,13 @@ class DirectLoginUseCase @Inject constructor( authenticationService.getWellKnownData(matrixId, config) } - private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) { - is WellknownResult.Prompt -> loginDirect(action, data, config) + private suspend fun createSessionFor(data: WellknownResult, action: LoginDirect, config: HomeServerConnectionConfig?) = when (data) { + is WellknownResult.Prompt -> loginDirect(action, data, config) is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config) - else -> onWellKnownError() + else -> onWellKnownError() } - private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result { + private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginDirect, config: HomeServerConnectionConfig?): Result { // Relax on IS discovery if homeserver is valid val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null return when { @@ -57,12 +57,12 @@ class DirectLoginUseCase @Inject constructor( } } - private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result { + private suspend fun loginDirect(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result { val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt) return runCatching { authenticationService.directAuthentication( alteredHomeServerConnectionConfig, - action.username, + action.matrixId, action.password, action.initialDeviceName ) @@ -74,8 +74,8 @@ class DirectLoginUseCase @Inject constructor( identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) } ) - private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig( - homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"), + private fun fallbackConfig(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig( + homeServerUri = uriFactory.parse("https://${action.matrixId.getServerName()}"), homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl), identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) } ) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index 9f7dce56ea..bef624ddc4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -46,9 +46,12 @@ sealed interface OnboardingAction : VectorViewModelAction { data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction object ResetPasswordMailConfirmed : OnboardingAction - // Login or Register, depending on the signMode - data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction - data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction + sealed interface AuthenticateAction : OnboardingAction { + data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction + data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction + data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction + } + object StopEmailValidationCheck : OnboardingAction data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index 6ffece4ab6..5dbcd162f3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -37,6 +37,7 @@ sealed class OnboardingViewEvents : VectorViewEvents { object OpenUseCaseSelection : OnboardingViewEvents() object OpenServerSelection : OnboardingViewEvents() object OpenCombinedRegister : OnboardingViewEvents() + object OpenCombinedLogin : OnboardingViewEvents() object EditServerSelection : OnboardingViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents() object OnLoginFlowRetrieved : OnboardingViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 03526b47a5..0bd61758bc 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.cancelCurrentOnSet import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel @@ -41,7 +42,9 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult +import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator import kotlinx.coroutines.Job import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -50,7 +53,6 @@ import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.FlowResult -import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session @@ -125,12 +127,8 @@ class OnboardingViewModel @AssistedInject constructor( private var loginConfig: LoginConfig? = null - private var currentJob: Job? = null - set(value) { - // Cancel any previous Job - field?.cancel() - field = value - } + private var emailVerificationPollingJob: Job? by cancelCurrentOnSet() + private var currentJob: Job? by cancelCurrentOnSet() override fun handle(action: OnboardingAction) { when (action) { @@ -142,8 +140,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) is OnboardingAction.InitWith -> handleInitWith(action) is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) } - is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } - is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action } + is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) } is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) @@ -168,6 +165,14 @@ class OnboardingViewModel @AssistedInject constructor( block(action) } + private fun handleAuthenticateAction(action: AuthenticateAction) { + when (action) { + is AuthenticateAction.Register -> handleRegisterWith(action) + is AuthenticateAction.Login -> handleLogin(action) + is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null) + } + } + private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) { if (resetConfig) { loginConfig = null @@ -191,16 +196,21 @@ class OnboardingViewModel @AssistedInject constructor( } private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) { - val nextOnboardingStep = when (onboardingFlow) { - OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) { - OnboardingViewEvents.OpenUseCaseSelection - } else { - OnboardingViewEvents.OpenServerSelection + when (onboardingFlow) { + OnboardingFlow.SignUp -> { + _viewEvents.post( + if (vectorFeatures.isOnboardingUseCaseEnabled()) { + OnboardingViewEvents.OpenUseCaseSelection + } else { + OnboardingViewEvents.OpenServerSelection + } + ) } - OnboardingFlow.SignIn, - OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection + OnboardingFlow.SignIn -> if (vectorFeatures.isOnboardingCombinedLoginEnabled()) { + handle(OnboardingAction.HomeServerChange.SelectHomeServer(defaultHomeserverUrl)) + } else _viewEvents.post(OnboardingViewEvents.OpenServerSelection) + OnboardingFlow.SignInSignUp -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection) } - _viewEvents.post(nextOnboardingStep) } private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) { @@ -212,7 +222,7 @@ class OnboardingViewModel @AssistedInject constructor( ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } ?.let { startAuthenticationFlow(finalLastAction, it) } } - is OnboardingAction.LoginOrRegister -> + is AuthenticateAction.LoginDirect -> handleDirectLogin( finalLastAction, HomeServerConnectionConfig.Builder() @@ -257,13 +267,19 @@ class OnboardingViewModel @AssistedInject constructor( } private fun handleRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { - currentJob = viewModelScope.launch { + val job = viewModelScope.launch { if (action.hasLoadingState()) { setState { copy(isLoading = true) } } internalRegisterAction(action, onNextRegistrationStepAction) setState { copy(isLoading = false) } } + + // Allow email verification polling to coexist with other jobs + when (action) { + is RegisterAction.CheckIfEmailHasBeenValidated -> emailVerificationPollingJob = job + else -> currentJob = job + } } private suspend fun internalRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { @@ -275,8 +291,10 @@ class OnboardingViewModel @AssistedInject constructor( // do nothing } else -> when (it) { - is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true) - is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) + is RegistrationResult.Complete -> onSessionCreated(it.session, isAccountCreated = true) + is RegistrationResult.NextStep -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) + is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationResult.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) } } }, @@ -289,10 +307,20 @@ class OnboardingViewModel @AssistedInject constructor( } private fun emitFlowResultViewEvent(flowResult: FlowResult) { - _viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted)) + withState { state -> + val orderedResult = when { + state.hasSelectedMatrixOrg() && vectorFeatures.isOnboardingCombinedRegisterEnabled() -> flowResult.copy( + missingStages = flowResult.missingStages.sortedWith(MatrixOrgRegistrationStagesComparator()) + ) + else -> flowResult + } + _viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(orderedResult, isRegistrationStarted)) + } } - private fun handleRegisterWith(action: OnboardingAction.Register) { + private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl + + private fun handleRegisterWith(action: AuthenticateAction.Register) { reAuthHelper.data = action.password handleRegisterAction( RegisterAction.CreateAccount( @@ -307,6 +335,7 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleResetAction(action: OnboardingAction.ResetAction) { // Cancel any request currentJob = null + emailVerificationPollingJob = null when (action) { OnboardingAction.ResetHomeServerType -> { @@ -466,16 +495,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleLoginOrRegister(action: OnboardingAction.LoginOrRegister) = withState { state -> - when (state.signMode) { - SignMode.Unknown -> error("Developer error, invalid sign mode") - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName)) - SignMode.SignInWithMatrixId -> handleDirectLogin(action, null) - } - } - - private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) { + private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) { setState { copy(isLoading = true) } currentJob = viewModelScope.launch { directLoginUseCase.execute(action, homeServerConnectionConfig).fold( @@ -488,7 +508,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleLogin(action: OnboardingAction.LoginOrRegister) { + private fun handleLogin(action: AuthenticateAction.Login) { val safeLoginWizard = loginWizard if (safeLoginWizard == null) { @@ -632,7 +652,11 @@ class OnboardingViewModel @AssistedInject constructor( when (trigger) { is OnboardingAction.HomeServerChange.EditHomeServer -> { when (awaitState().onboardingFlow) { - OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { _ -> + OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { + updateServerSelection(config, serverTypeOverride, authResult) + _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited) + } + OnboardingFlow.SignIn -> { updateServerSelection(config, serverTypeOverride, authResult) _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited) } @@ -645,7 +669,10 @@ class OnboardingViewModel @AssistedInject constructor( when (awaitState().onboardingFlow) { OnboardingFlow.SignIn -> { updateSignMode(SignMode.SignIn) - _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn)) + when (vectorFeatures.isOnboardingCombinedLoginEnabled()) { + true -> _viewEvents.post(OnboardingViewEvents.OpenCombinedLogin) + false -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn)) + } } OnboardingFlow.SignUp -> { updateSignMode(SignMode.SignUp) @@ -790,7 +817,7 @@ class OnboardingViewModel @AssistedInject constructor( } private fun cancelWaitForEmailValidation() { - currentJob = null + emailVerificationPollingJob = null } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index b4998d2ba0..7bffe50754 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -16,26 +16,80 @@ package im.vector.app.features.onboarding +import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.RegisterThreePid -import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.FlowResponse +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.Success import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.failure.is401 +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject +import org.matrix.android.sdk.api.auth.registration.RegistrationResult as MatrixRegistrationResult class RegistrationActionHandler @Inject constructor() { suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { return when (action) { - RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow() - is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse) - is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms() - is RegisterAction.RegisterDummy -> registrationWizard.dummy() - is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid) - is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid() - is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code) - is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) - is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName) + RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } + is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } + is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } + is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } + is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) + is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } + is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } + is RegisterAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailIsValidated(registrationWizard, action.delayMillis) + is RegisterAction.CreateAccount -> resultOf { + registrationWizard.createAccount( + action.username, + action.password, + action.initialDeviceName + ) + } } } + + private suspend fun handleAddThreePid(wizard: RegistrationWizard, action: RegisterAction.AddThreePid): RegistrationResult { + return runCatching { wizard.addThreePid(action.threePid) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) + else -> RegistrationResult.Error(it) + } + } + ) + } + + private tailrec suspend fun handleCheckIfEmailIsValidated(registrationWizard: RegistrationWizard, delayMillis: Long): RegistrationResult { + return runCatching { registrationWizard.checkIfEmailHasBeenValidated(delayMillis) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + it.is401() -> null // recursively continue to check with a delay + else -> RegistrationResult.Error(it) + } + } + ) ?: handleCheckIfEmailIsValidated(registrationWizard, 10_000) + } +} + +private inline fun resultOf(block: () -> MatrixRegistrationResult): RegistrationResult { + return runCatching { block() }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { RegistrationResult.Error(it) } + ) +} + +private fun MatrixRegistrationResult.toRegistrationResult() = when (this) { + is FlowResponse -> RegistrationResult.NextStep(flowResult) + is Success -> RegistrationResult.Complete(session) +} + +sealed interface RegistrationResult { + data class Error(val cause: Throwable) : RegistrationResult + data class Complete(val session: Session) : RegistrationResult + data class NextStep(val flowResult: FlowResult) : RegistrationResult + data class SendEmailSuccess(val email: String) : RegistrationResult } sealed interface RegisterAction { @@ -56,7 +110,6 @@ sealed interface RegisterAction { } fun RegisterAction.ignoresResult() = when (this) { - is RegisterAction.AddThreePid -> true is RegisterAction.SendAgainThreePid -> true else -> false } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt index 1ce0c544e5..f4cf1e9bea 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt @@ -22,7 +22,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import com.google.android.material.textfield.TextInputLayout +import im.vector.app.core.extensions.hasContent import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.databinding.FragmentFtueDisplayNameBinding import im.vector.app.features.onboarding.OnboardingAction @@ -69,7 +69,7 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth override fun updateWithState(state: OnboardingViewState) { views.displayNameInput.editText?.setText(state.personalizationState.displayName) - views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty() + views.displayNameSubmit.isEnabled = views.displayNameInput.hasContent() } override fun resetViewModel() { @@ -81,5 +81,3 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth return true } } - -private fun TextInputLayout.hasContentEmpty() = !editText?.text.isNullOrEmpty() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt new file mode 100644 index 0000000000..7324c4fbb1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -0,0 +1,161 @@ +/* + * 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.onboarding.ftueauth + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.autofill.HintConstants +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import im.vector.app.R +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.databinding.FragmentFtueCombinedLoginBinding +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SSORedirectRouterActivity +import im.vector.app.features.login.SocialLoginButtonsView +import im.vector.app.features.login.render +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.OnboardingViewState +import kotlinx.coroutines.flow.launchIn +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import javax.inject.Inject + +class FtueAuthCombinedLoginFragment @Inject constructor( + private val loginFieldsValidation: LoginFieldsValidation, + private val loginErrorParser: LoginErrorParser +) : AbstractSSOFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedLoginBinding { + return FragmentFtueCombinedLoginBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupSubmitButton() + views.loginRoot.realignPercentagesToParent() + views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } + views.loginPasswordInput.setOnImeDoneListener { submit() } + } + + private fun setupSubmitButton() { + views.loginSubmit.setOnClickListener { submit() } + observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit) + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun submit() { + cleanupUi() + loginFieldsValidation.validate(views.loginInput.content(), views.loginPasswordInput.content()) + .onUsernameOrIdError { views.loginInput.error = it } + .onPasswordError { views.loginPasswordInput.error = it } + .onValid { usernameOrId, password -> + val initialDeviceName = getString(R.string.login_default_session_public_name) + viewModel.handle(OnboardingAction.AuthenticateAction.Login(usernameOrId, password, initialDeviceName)) + } + } + + private fun cleanupUi() { + views.loginSubmit.hideKeyboard() + views.loginInput.error = null + views.loginPasswordInput.error = null + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } + + override fun onError(throwable: Throwable) { + // Trick to display the error without text. + views.loginInput.error = " " + loginErrorParser.parse(throwable, views.loginPasswordInput.content()) + .onUnknown { super.onError(it) } + .onUsernameOrIdError { views.loginInput.error = it } + .onPasswordError { views.loginPasswordInput.error = it } + } + + override fun updateWithState(state: OnboardingViewState) { + setupUi(state) + setupAutoFill() + + views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl() + views.selectedServerDescription.text = state.selectedHomeserver.description + + if (state.isLoading) { + // Ensure password is hidden + views.loginPasswordInput.editText().hidePassword() + } + } + + private fun setupUi(state: OnboardingViewState) { + when (state.selectedHomeserver.preferredLoginMode) { + is LoginMode.SsoAndPassword -> { + showUsernamePassword() + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) + } + is LoginMode.Sso -> { + hideUsernamePassword() + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) + } + else -> { + showUsernamePassword() + hideSsoProviders() + } + } + } + + private fun renderSsoProviders(deviceId: String?, ssoProviders: List?) { + views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true + views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible + views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> + viewModel.getSsoUrl( + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, + deviceId = deviceId, + providerId = id + )?.let { openInCustomTab(it) } + } + } + + private fun hideSsoProviders() { + views.ssoGroup.isVisible = false + views.ssoButtons.ssoIdentityProviders = null + } + + private fun hideUsernamePassword() { + views.loginEntryGroup.isVisible = false + } + + private fun showUsernamePassword() { + views.loginEntryGroup.isVisible = true + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) + views.loginPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 0755f18c8c..62aa0854c3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible @@ -31,22 +30,22 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText -import im.vector.app.core.extensions.hasContentFlow import im.vector.app.core.extensions.hasSurroundingSpaces import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.realignPercentagesToParent +import im.vector.app.core.extensions.setOnImeDoneListener import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView +import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername @@ -66,36 +65,16 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu super.onViewCreated(view, savedInstanceState) setupSubmitButton() views.createAccountRoot.realignPercentagesToParent() - views.editServerButton.debouncedClicks { - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) - } - - views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - submit() - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } + views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } + views.createAccountPasswordInput.setOnImeDoneListener { submit() } } private fun setupSubmitButton() { views.createAccountSubmit.setOnClickListener { submit() } - observeInputFields() - .onEach { - views.createAccountPasswordInput.error = null - views.createAccountInput.error = null - views.createAccountSubmit.isEnabled = it - } + observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit) .launchIn(viewLifecycleOwner.lifecycleScope) } - private fun observeInputFields() = combine( - views.createAccountInput.hasContentFlow { it.trim() }, - views.createAccountPasswordInput.hasContentFlow(), - transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty } - ) - private fun submit() { withState(viewModel) { state -> cleanupUi() @@ -119,7 +98,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu } if (error == 0) { - viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name))) + viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name))) } } } @@ -185,9 +164,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu private fun renderSsoProviders(deviceId: String?, ssoProviders: List?) { views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true - views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE - views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted() - views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id -> + views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> viewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt index 2e6057288a..b7a5dc7298 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt @@ -68,7 +68,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt views.chooseServerSubmit.debouncedClicks { updateServerUrl() } views.chooseServerInput.editText().textChanges() .onEach { views.chooseServerInput.error = null } - .launchIn(lifecycleScope) + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun updateServerUrl() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt new file mode 100644 index 0000000000..ea376709f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import im.vector.app.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueEmailInputBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import reactivecircus.flowbinding.android.widget.textChanges +import javax.inject.Inject + +class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueEmailInputBinding { + return FragmentFtueEmailInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit) + views.emailEntryInput.setOnImeDoneListener { updateEmail() } + views.emailEntrySubmit.debouncedClicks { updateEmail() } + + views.emailEntryInput.editText().textChanges() + .onEach { + views.emailEntryInput.error = null + views.emailEntrySubmit.isEnabled = it.isEmail() + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun updateEmail() { + val email = views.emailEntryInput.content() + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(email)))) + } + + override fun onError(throwable: Throwable) { + views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index ce3dee7a19..fce1308d3c 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -223,12 +223,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA override fun onError(throwable: Throwable) { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - if (throwable.is401()) { - // This is normal use case, we go to the mail waiting screen - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(viewModel.currentThreePid ?: ""))) - } else { - views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) - } + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } TextInputFormFragmentMode.SetMsisdn -> { if (throwable.is401()) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt new file mode 100644 index 0000000000..c815f354f0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.databinding.FragmentLoginWaitForEmailBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction +import javax.inject.Inject + +/** + * In this screen, the user is asked to check their emails. + */ +class FtueAuthLegacyWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragment() { + + private val params: FtueAuthWaitForEmailFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmailBinding { + return FragmentLoginWaitForEmailBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + } + + override fun onResume() { + super.onResume() + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) + } + + override fun onPause() { + super.onPause() + viewModel.handle(OnboardingAction.StopEmailValidationCheck) + } + + private fun setupUi() { + views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index 2308280400..98d9a24999 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -26,6 +26,7 @@ import androidx.autofill.HintConstants import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard @@ -119,40 +120,43 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment< } private fun submit() { - cleanupUi() + withState(viewModel) { state -> + cleanupUi() - val login = views.loginField.text.toString() - val password = views.passwordField.text.toString() + val login = views.loginField.text.toString() + val password = views.passwordField.text.toString() - // This can be called by the IME action, so deal with empty cases - var error = 0 - if (login.isEmpty()) { - views.loginFieldTil.error = getString( - if (isSignupMode) { - R.string.error_empty_field_choose_user_name - } else { - R.string.error_empty_field_enter_user_name - } - ) - error++ - } - if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { - views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username) - error++ - } - if (password.isEmpty()) { - views.passwordFieldTil.error = getString( - if (isSignupMode) { - R.string.error_empty_field_choose_password - } else { - R.string.error_empty_field_your_password - } - ) - error++ - } + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (login.isEmpty()) { + views.loginFieldTil.error = getString( + if (isSignupMode) { + R.string.error_empty_field_choose_user_name + } else { + R.string.error_empty_field_enter_user_name + } + ) + error++ + } + if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { + views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username) + error++ + } + if (password.isEmpty()) { + views.passwordFieldTil.error = getString( + if (isSignupMode) { + R.string.error_empty_field_choose_password + } else { + R.string.error_empty_field_your_password + } + ) + error++ + } - if (error == 0) { - viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name))) + if (error == 0) { + val initialDeviceName = getString(R.string.login_default_session_public_name) + viewModel.handle(state.signMode.toAuthenticateAction(login, password, initialDeviceName)) + } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 49e8875cb5..30416bde9e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -22,6 +22,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 import com.airbnb.mvrx.withState @@ -90,7 +92,7 @@ class FtueAuthSplashCarouselFragment @Inject constructor( private fun ViewPager2.registerAutomaticUntilInteractionTransitions() { var scheduledTransition: Job? = null - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + val pageChangingCallback = object : ViewPager2.OnPageChangeCallback() { private var hasUserManuallyInteractedWithCarousel: Boolean = false override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { @@ -104,12 +106,21 @@ class FtueAuthSplashCarouselFragment @Inject constructor( scheduledTransition = scheduleCarouselTransition() } } + } + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + registerOnPageChangeCallback(pageChangingCallback) + } + + override fun onDestroy(owner: LifecycleOwner) { + unregisterOnPageChangeCallback(pageChangingCallback) + } }) } private fun ViewPager2.scheduleCarouselTransition(): Job { val itemCount = adapter?.itemCount ?: throw IllegalStateException("An adapter must be set") - return lifecycleScope.launch { + return viewLifecycleOwner.lifecycleScope.launch { delay(CAROUSEL_ROTATION_DELAY_MS) setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 6f1b85df4f..5ad6b7e78d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -54,7 +54,6 @@ import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthLegacyStyleTermsFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsLegacyStyleFragmentArgument -import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms import org.matrix.android.sdk.api.extensions.tryOrNull @@ -192,12 +191,7 @@ class FtueAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } is OnboardingViewEvents.OnSendEmailSuccess -> { - // Pop the enter email Fragment - supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - addRegistrationStageFragmentToBackstack( - FtueAuthWaitForEmailFragment::class.java, - FtueAuthWaitForEmailFragmentArgument(viewEvents.email), - ) + openWaitForEmailVerification(viewEvents.email) } is OnboardingViewEvents.OnSendMsisdnSuccess -> { // Pop the enter Msisdn Fragment @@ -233,24 +227,24 @@ class FtueAuthVariant( option = commonOption ) } - OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() + OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() + OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() } } + private fun onStartCombinedLogin() { + addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java) + } + private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) { when { registrationShouldFallback(viewEvents) -> displayFallbackWebDialog() - viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.orderedStages()) + viewEvents.isRegistrationStarted -> handleRegistrationNavigation(viewEvents.flowResult.missingStages) vectorFeatures.isOnboardingCombinedRegisterEnabled() -> openStartCombinedRegister() else -> openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG) } } - private fun FlowResult.orderedStages() = when { - vectorFeatures.isOnboardingCombinedRegisterEnabled() -> missingStages.sortedWith(FtueMissingRegistrationStagesComparator()) - else -> missingStages - } - private fun openStartCombinedRegister() { addRegistrationStageFragmentToBackstack(FtueAuthCombinedRegisterFragment::class.java) } @@ -393,10 +387,7 @@ class FtueAuthVariant( when (stage) { is Stage.ReCaptcha -> onCaptcha(stage) - is Stage.Email -> addRegistrationStageFragmentToBackstack( - FtueAuthGenericTextInputFormFragment::class.java, - FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), - ) + is Stage.Email -> onEmail(stage) is Stage.Msisdn -> addRegistrationStageFragmentToBackstack( FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), @@ -406,6 +397,32 @@ class FtueAuthVariant( } } + private fun onEmail(stage: Stage) { + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthEmailEntryFragment::class.java + ) + else -> addRegistrationStageFragmentToBackstack( + FtueAuthGenericTextInputFormFragment::class.java, + FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + ) + } + } + + private fun openWaitForEmailVerification(email: String) { + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthWaitForEmailFragment::class.java, + FtueAuthWaitForEmailFragmentArgument(email), + ) + else -> addRegistrationStageFragmentToBackstack( + FtueAuthLegacyWaitForEmailFragment::class.java, + FtueAuthWaitForEmailFragmentArgument(email), + ) + } + } + private fun onTerms(stage: Stage.Terms) { when { vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index d78e0fe74d..c81a9c2feb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -21,13 +21,16 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.app.R -import im.vector.app.databinding.FragmentLoginWaitForEmailBinding +import im.vector.app.core.utils.colorTerminatingFullStop +import im.vector.app.databinding.FragmentFtueWaitForEmailVerificationBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.RegisterAction +import im.vector.app.features.themes.ThemeProvider +import im.vector.app.features.themes.ThemeUtils import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject @Parcelize @@ -38,45 +41,57 @@ data class FtueAuthWaitForEmailFragmentArgument( /** * In this screen, the user is asked to check their emails. */ -class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragment() { +class FtueAuthWaitForEmailFragment @Inject constructor( + private val themeProvider: ThemeProvider +) : AbstractFtueAuthFragment() { private val params: FtueAuthWaitForEmailFragmentArgument by args() + private var inferHasLeftAndReturnedToScreen = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmailBinding { - return FragmentLoginWaitForEmailBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding { + return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupUi() } + private fun setupUi() { + views.emailVerificationGradientContainer.setBackgroundResource( + when (themeProvider.isLightTheme()) { + true -> R.drawable.bg_waiting_for_email_verification + false -> R.drawable.bg_color_background + } + ) + views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title) + .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) + views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) + views.emailVerificationResendEmail.debouncedClicks { + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid)) + } + } + override fun onResume() { super.onResume() - + showLoadingIfReturningToScreen() viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) } + private fun showLoadingIfReturningToScreen() { + when (inferHasLeftAndReturnedToScreen) { + true -> views.emailVerificationWaiting.isVisible = true + false -> { + inferHasLeftAndReturnedToScreen = true + } + } + } + override fun onPause() { super.onPause() - viewModel.handle(OnboardingAction.StopEmailValidationCheck) } - private fun setupUi() { - views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email) - } - - override fun onError(throwable: Throwable) { - if (throwable.is401()) { - // Try again, with a delay - viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000))) - } else { - super.onError(throwable) - } - } - override fun resetViewModel() { viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt new file mode 100644 index 0000000000..8d63fbf547 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import android.widget.Button +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.core.extensions.hasContentFlow +import im.vector.app.features.login.SignMode +import im.vector.app.features.onboarding.OnboardingAction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach + +fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction { + return when (this) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> OnboardingAction.AuthenticateAction.Register(username = login, password, initialDeviceName) + SignMode.SignIn -> OnboardingAction.AuthenticateAction.Login(username = login, password, initialDeviceName) + SignMode.SignInWithMatrixId -> OnboardingAction.AuthenticateAction.LoginDirect(matrixId = login, password, initialDeviceName) + } +} + +/** + * A flow to monitor content changes from both username/id and password fields, + * clearing errors and enabling/disabling the submission button on non empty content changes. + */ +fun observeContentChangesAndResetErrors(username: TextInputLayout, password: TextInputLayout, submit: Button): Flow<*> { + return combine( + username.hasContentFlow { it.trim() }, + password.hasContentFlow(), + transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent } + ).onEach { + username.error = null + password.error = null + submit.isEnabled = it + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt new file mode 100644 index 0000000000..a92fdea04a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginErrorParser.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import im.vector.app.R +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult +import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.failure.isInvalidUsername +import org.matrix.android.sdk.api.failure.isLoginEmailUnknown +import javax.inject.Inject + +class LoginErrorParser @Inject constructor( + private val errorFormatter: ErrorFormatter, + private val stringProvider: StringProvider, +) { + fun parse(throwable: Throwable, password: String): LoginErrorResult { + return when { + throwable.isInvalidUsername() -> { + LoginErrorResult(throwable, usernameOrIdError = errorFormatter.toHumanReadable(throwable)) + } + throwable.isLoginEmailUnknown() -> { + LoginErrorResult(throwable, usernameOrIdError = stringProvider.getString(R.string.login_login_with_email_error)) + } + throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> { + LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password)) + } + else -> { + LoginErrorResult(throwable) + } + } + } + + private fun String.hasSurroundingSpaces() = trim() != this + + data class LoginErrorResult(val cause: Throwable, val usernameOrIdError: String? = null, val passwordError: String? = null) +} + +fun LoginErrorResult.onUnknown(action: (Throwable) -> Unit): LoginErrorResult { + when { + usernameOrIdError == null && passwordError == null -> action(cause) + } + return this +} + +fun LoginErrorResult.onUsernameOrIdError(action: (String) -> Unit): LoginErrorResult { + usernameOrIdError?.let(action) + return this +} + +fun LoginErrorResult.onPasswordError(action: (String) -> Unit): LoginErrorResult { + passwordError?.let(action) + return this +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginFieldsValidation.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginFieldsValidation.kt new file mode 100644 index 0000000000..659a8cd2c1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginFieldsValidation.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class LoginFieldsValidation @Inject constructor( + private val stringProvider: StringProvider +) { + + fun validate(usernameOrId: String, password: String): LoginValidationResult { + return LoginValidationResult(usernameOrId, password, validateUsernameOrId(usernameOrId), validatePassword(password)) + } + + private fun validateUsernameOrId(usernameOrId: String): String? { + val accountError = when { + usernameOrId.isEmpty() -> stringProvider.getString(R.string.error_empty_field_enter_user_name) + else -> null + } + return accountError + } + + private fun validatePassword(password: String): String? { + val passwordError = when { + password.isEmpty() -> stringProvider.getString(R.string.error_empty_field_your_password) + else -> null + } + return passwordError + } +} + +fun LoginValidationResult.onValid(action: (String, String) -> Unit): LoginValidationResult { + when { + usernameOrIdError == null && passwordError == null -> action(usernameOrId, password) + } + return this +} + +fun LoginValidationResult.onUsernameOrIdError(action: (String) -> Unit): LoginValidationResult { + usernameOrIdError?.let(action) + return this +} + +fun LoginValidationResult.onPasswordError(action: (String) -> Unit): LoginValidationResult { + passwordError?.let(action) + return this +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginValidationResult.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginValidationResult.kt new file mode 100644 index 0000000000..caf127332a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/LoginValidationResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding.ftueauth + +data class LoginValidationResult( + val usernameOrId: String, + val password: String, + val usernameOrIdError: String?, + val passwordError: String? +) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt similarity index 84% rename from vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt rename to vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt index 6a6326625e..527c20987a 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueMissingRegistrationStagesComparator.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/MatrixOrgRegistrationStagesComparator.kt @@ -18,10 +18,10 @@ package im.vector.app.features.onboarding.ftueauth import org.matrix.android.sdk.api.auth.registration.Stage -class FtueMissingRegistrationStagesComparator : Comparator { +class MatrixOrgRegistrationStagesComparator : Comparator { - override fun compare(a: Stage?, b: Stage?): Int { - return (a?.toPriority() ?: 0) - (b?.toPriority() ?: 0) + override fun compare(a: Stage, b: Stage): Int { + return a.toPriority().compareTo(b.toPriority()) } private fun Stage.toPriority() = when (this) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt index 23f7014374..f8b885ddee 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt @@ -23,11 +23,11 @@ import im.vector.app.R import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.isEnglishSpeaking +import im.vector.app.core.utils.colorTerminatingFullStop import im.vector.app.features.themes.ThemeProvider import im.vector.app.features.themes.ThemeUtils import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence -import me.gujun.android.span.span import javax.inject.Inject class SplashCarouselStateFactory @Inject constructor( @@ -39,7 +39,7 @@ class SplashCarouselStateFactory @Inject constructor( fun create(): SplashCarouselState { val lightTheme = themeProvider.isLightTheme() - fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_carousel_page_dark + fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_color_background fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable return SplashCarouselState( listOf( @@ -79,18 +79,8 @@ class SplashCarouselStateFactory @Inject constructor( } private fun Int.colorTerminatingFullStop(@AttrRes color: Int): EpoxyCharSequence { - val string = stringProvider.getString(this) - val fullStop = "." - val charSequence = if (string.endsWith(fullStop)) { - span { - +string.removeSuffix(fullStop) - span(fullStop) { - textColor = ThemeUtils.getColor(context, color) - } - } - } else { - string - } - return charSequence.toEpoxyCharSequence() + return stringProvider.getString(this) + .colorTerminatingFullStop(ThemeUtils.getColor(context, color)) + .toEpoxyCharSequence() } } diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index b7ce7ffdb4..e2c1aaa2a4 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -156,13 +156,16 @@ class BugReporter @Inject constructor( /** * Send a bug report. * - * @param reportType The report type (bug, suggestion, feedback) - * @param withDevicesLogs true to include the device log - * @param withCrashLogs true to include the crash logs + * @param reportType The report type (bug, suggestion, feedback) + * @param withDevicesLogs true to include the device log + * @param withCrashLogs true to include the crash logs * @param withKeyRequestHistory true to include the crash logs - * @param withScreenshot true to include the screenshot + * @param withScreenshot true to include the screenshot * @param theBugDescription the bug description - * @param listener the listener + * @param serverVersion version of the server + * @param canContact true if the user opt in to be contacted directly + * @param customFields fields which will be sent with the report + * @param listener the listener */ @SuppressLint("StaticFieldLeak") fun sendBugReport(reportType: ReportType, @@ -287,7 +290,8 @@ class BugReporter @Inject constructor( .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) - .addFormDataPart("server_version", serverVersion).apply { + .addFormDataPart("server_version", serverVersion) + .apply { customFields?.forEach { (name, value) -> addFormDataPart(name, value) } @@ -678,7 +682,7 @@ class BugReporter @Inject constructor( /** * Retrieves the logs. * - * @param streamWriter the stream writer + * @param streamWriter the stream writer * @param isErrorLogCat true to save the error logs */ private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporterMultipartBody.java b/vector/src/main/java/im/vector/app/features/rageshake/BugReporterMultipartBody.java index a530b6e667..72cc63e5c7 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporterMultipartBody.java +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporterMultipartBody.java @@ -39,7 +39,7 @@ public class BugReporterMultipartBody extends RequestBody { /** * Upload listener * - * @param totalWritten total written bytes + * @param totalWritten total written bytes * @param contentLength content length */ void onWrite(long totalWritten, long contentLength); @@ -296,4 +296,4 @@ public class BugReporterMultipartBody extends RequestBody { return new BugReporterMultipartBody(boundary, parts); } } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt index bc78b84088..5496ff4a94 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt @@ -57,7 +57,7 @@ class VectorUncaughtExceptionHandler @Inject constructor( /** * An uncaught exception has been triggered. * - * @param thread the thread + * @param thread the thread * @param throwable the throwable * @return the exception description */ diff --git a/vector/src/main/java/im/vector/app/features/settings/FontScale.kt b/vector/src/main/java/im/vector/app/features/settings/FontScale.kt index c4ea730afd..a1acef7d35 100644 --- a/vector/src/main/java/im/vector/app/features/settings/FontScale.kt +++ b/vector/src/main/java/im/vector/app/features/settings/FontScale.kt @@ -78,6 +78,7 @@ object FontScale { /** * Store the font scale value. * + * @param context the Android context * @param fontScaleValue the font scale value to store */ private fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt index 3fb3d3f7c8..326f20845f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt @@ -120,8 +120,8 @@ object VectorLocale { /** * Get String from a locale. * - * @param context the context - * @param locale the locale + * @param context the context + * @param locale the locale * @param resourceId the string resource id * @return the localized string */ diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index c841c6a0af..72f6080417 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -203,6 +203,7 @@ class VectorPreferences @Inject constructor( private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" + private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" // This key will be used to identify clients with the old thread support enabled io.element.thread const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" @@ -492,7 +493,7 @@ class VectorPreferences @Inject constructor( /** * Update the notification ringtone. * - * @param uri the new notification ringtone, or null for no RingTone + * @param uri the new notification ringtone, or null for no RingTone */ fun setNotificationRingTone(uri: Uri?) { defaultPrefs.edit { @@ -635,7 +636,7 @@ class VectorPreferences @Inject constructor( /** * Tells if the application is started on boot. * - * @param value true to start the application on boot + * @param value true to start the application on boot */ fun setAutoStartOnBoot(value: Boolean) { defaultPrefs.edit { @@ -655,7 +656,7 @@ class VectorPreferences @Inject constructor( /** * Updates the selected saving period. * - * @param index the selected period index + * @param index the selected period index */ fun setSelectedMediasSavingPeriod(index: Int) { defaultPrefs.edit { @@ -1041,6 +1042,10 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) } + fun labsEnableLiveLocation(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_LIVE_LOCATION, false) + } + /** * Indicates whether or not thread messages are enabled. */ diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index 3b1e8240fa..3d1a224d0c 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -108,6 +108,7 @@ object ThemeUtils { /** * Update the application theme. * + * @param context the Android context * @param aTheme the new theme */ fun setApplicationTheme(context: Context, aTheme: String) { @@ -126,9 +127,11 @@ object ThemeUtils { } /** - * Set the activity theme according to the selected one. + * Set the activity theme according to the selected one. Default is Light, so if this is the current + * theme, the theme is not changed. * * @param activity the activity + * @param otherThemes themes to apply for dark and black theme */ fun setActivityTheme(activity: Activity, otherThemes: ActivityOtherThemes) { when (getApplicationTheme(activity)) { @@ -143,7 +146,7 @@ object ThemeUtils { /** * Translates color attributes to colors. * - * @param c Context + * @param c Context * @param colorAttribute Color Attribute * @return Requested Color */ @@ -175,8 +178,8 @@ object ThemeUtils { /** * Tint the drawable with a theme attribute. * - * @param context the context - * @param drawable the drawable to tint + * @param context the context + * @param drawable the drawable to tint * @param attribute the theme color * @return the tinted drawable */ @@ -188,7 +191,7 @@ object ThemeUtils { * Tint the drawable with a color integer. * * @param drawable the drawable to tint - * @param color the color + * @param color the color * @return the tinted drawable */ fun tintDrawableWithColor(drawable: Drawable, @ColorInt color: Int): Drawable { diff --git a/vector/src/main/java/im/vector/app/features/webview/WebViewEventListener.kt b/vector/src/main/java/im/vector/app/features/webview/WebViewEventListener.kt index bd77283029..2f00ad07b9 100644 --- a/vector/src/main/java/im/vector/app/features/webview/WebViewEventListener.kt +++ b/vector/src/main/java/im/vector/app/features/webview/WebViewEventListener.kt @@ -48,8 +48,8 @@ interface WebViewEventListener { /** * Triggered when an error occurred while loading a page. * - * @param url The url that failed. - * @param errorCode The error code. + * @param url The url that failed. + * @param errorCode The error code. * @param description The error description. */ fun onPageError(url: String, errorCode: Int, description: String) { @@ -59,8 +59,8 @@ interface WebViewEventListener { /** * Triggered when an error occurred while loading a page. * - * @param url The url that failed. - * @param errorCode The error code. + * @param url The url that failed. + * @param errorCode The error code. * @param description The error description. */ fun onHttpError(url: String, errorCode: Int, description: String) { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 3c88ea65a3..fc73e71b51 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -110,6 +110,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Retrieve the latest botOptions event. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun getBotOptions(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -171,6 +172,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Provides the membership state. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun getMembershipState(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -190,6 +192,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Request the latest joined room event. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun getJoinRules(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -208,6 +211,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Provide the widgets list. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun getWidgets(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -228,6 +232,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Set a new widget. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun setWidget(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -303,6 +308,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Update the 'plumbing state". * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun setPlumbingState(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -328,6 +334,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Update the bot options. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ @Suppress("UNCHECKED_CAST") @@ -353,6 +360,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Update the bot power levels. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun setBotPower(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -375,6 +383,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Invite an user to this room. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun inviteUser(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { @@ -397,6 +406,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo /** * Provides the number of members in the rooms. * + * @param widgetPostAPIMediator the post api mediator * @param eventData the modular data */ private fun getMembershipCount(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) { diff --git a/vector/src/main/res/drawable/ic_email.xml b/vector/src/main/res/drawable/ic_email.xml new file mode 100644 index 0000000000..48de7aec41 --- /dev/null +++ b/vector/src/main/res/drawable/ic_email.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml new file mode 100644 index 0000000000..1b65056e9f --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + +